PluginProbe
PhastPress / 1.86
PhastPress v1.86
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.86, at sdk/phast.php

10,080 lines 394.8 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 public 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\MetaCharset\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(), '~') . '/~']]];
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 $cdnLoop = ['Phast'];
1158 if (!empty($_SERVER['HTTP_CDN_LOOP'])) {
1159 $cdnLoop[] = $_SERVER['HTTP_CDN_LOOP'];
1160 }
1161 try {
1162 $response = $this->client->get($url, ['User-Agent' => 'Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:56.0) Gecko/20100101 Firefox/56.0', 'CDN-Loop' => implode(', ', $cdnLoop)]);
1163 } catch (\Exception $e) {
1164 $this->logger()->warning('Caught {cls} while fetching {url}: ({code}) {message}', ['cls' => get_class($e), 'url' => (string) $url, 'code' => $e->getCode(), 'message' => $e->getMessage()]);
1165 return false;
1166 }
1167 return $response->getContent();
1168 }
1169 }
1170 namespace Kibo\Phast\Retrievers;
1171
1172 class RemoteRetrieverFactory
1173 {
1174 public function make(array $config)
1175 {
1176 return new \Kibo\Phast\Retrievers\RemoteRetriever((new \Kibo\Phast\HTTP\ClientFactory())->make($config));
1177 }
1178 }
1179 namespace Kibo\Phast\Retrievers;
1180
1181 class UniversalRetriever implements \Kibo\Phast\Retrievers\Retriever
1182 {
1183 /**
1184 * @var Retriever[]
1185 */
1186 private $retrievers = array();
1187 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
1188 {
1189 return $this->iterateRetrievers(function (\Kibo\Phast\Retrievers\Retriever $retriever) use($url) {
1190 return $retriever->retrieve($url);
1191 });
1192 }
1193 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
1194 {
1195 return $this->iterateRetrievers(function (\Kibo\Phast\Retrievers\Retriever $retriever) use($url) {
1196 return $retriever->getCacheSalt($url);
1197 });
1198 }
1199 private function iterateRetrievers(callable $callback)
1200 {
1201 foreach ($this->retrievers as $retriever) {
1202 $result = $callback($retriever);
1203 if ($result !== false) {
1204 return $result;
1205 }
1206 }
1207 return false;
1208 }
1209 public function addRetriever(\Kibo\Phast\Retrievers\Retriever $retriever)
1210 {
1211 $this->retrievers[] = $retriever;
1212 }
1213 }
1214 namespace Kibo\Phast\Retrievers;
1215
1216 class LocalRetriever implements \Kibo\Phast\Retrievers\Retriever
1217 {
1218 /**
1219 * @var array
1220 */
1221 private $map;
1222 /**
1223 * @var ObjectifiedFunctions
1224 */
1225 private $funcs;
1226 /**
1227 * LocalRetriever constructor.
1228 *
1229 * @param array $map
1230 * @param ObjectifiedFunctions|null $functions
1231 */
1232 public function __construct(array $map, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
1233 {
1234 $this->map = $map;
1235 if ($functions) {
1236 $this->funcs = $functions;
1237 } else {
1238 $this->funcs = new \Kibo\Phast\Common\ObjectifiedFunctions();
1239 }
1240 }
1241 public static function getAllowedExtensions()
1242 {
1243 return ['css', 'js', 'bmp', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico', 'svg', 'txt'];
1244 }
1245 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
1246 {
1247 return $this->guard($url, function ($file) {
1248 return @$this->funcs->file_get_contents($file);
1249 });
1250 }
1251 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
1252 {
1253 return $this->guard($url, function ($file) {
1254 $size = @$this->funcs->filesize($file);
1255 $mtime = @$this->funcs->filectime($file);
1256 if ($size === false && $mtime === false) {
1257 return '';
1258 }
1259 return "{$mtime}-{$size}";
1260 });
1261 }
1262 public function getSize(\Kibo\Phast\ValueObjects\URL $url)
1263 {
1264 return $this->guard($url, function ($file) {
1265 return @$this->funcs->filesize($file);
1266 });
1267 }
1268 private function guard(\Kibo\Phast\ValueObjects\URL $url, callable $cb)
1269 {
1270 if (!in_array($this->getExtensionForURL($url), self::getAllowedExtensions())) {
1271 return false;
1272 }
1273 $file = $this->getFileForURL($url);
1274 if ($file === false) {
1275 return false;
1276 }
1277 return $cb($file);
1278 }
1279 private function getExtensionForURL(\Kibo\Phast\ValueObjects\URL $url)
1280 {
1281 $dotPosition = strrpos($url->getDecodedPath(), '.');
1282 if ($dotPosition === false) {
1283 return '';
1284 }
1285 return strtolower(substr($url->getDecodedPath(), $dotPosition + 1));
1286 }
1287 private function getFileForURL(\Kibo\Phast\ValueObjects\URL $url)
1288 {
1289 if (!isset($this->map[$url->getHost()])) {
1290 return false;
1291 }
1292 $submap = $this->map[$url->getHost()];
1293 if (!is_array($submap)) {
1294 return $this->appendNormalized($submap, $url->getDecodedPath());
1295 }
1296 $selectedPath = null;
1297 $selectedRoot = null;
1298 foreach ($submap as $prefix => $root) {
1299 $pattern = '~^(?=/)/*?(?:' . str_replace('~', '\\~', $prefix) . ')(?<path>/*(?<=/).*)~';
1300 if (preg_match($pattern, $url->getDecodedPath(), $match) && ($selectedPath === null || strlen($match['path']) < strlen($selectedPath))) {
1301 $selectedRoot = $root;
1302 $selectedPath = $match['path'];
1303 }
1304 }
1305 if ($selectedPath === null) {
1306 return false;
1307 }
1308 return $this->appendNormalized($selectedRoot, $selectedPath);
1309 }
1310 private function appendNormalized($target, $appended)
1311 {
1312 $appended = explode("\0", $appended)[0];
1313 $appended = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $appended);
1314 $absolutes = [];
1315 foreach (explode(DIRECTORY_SEPARATOR, $appended) as $part) {
1316 if ($part == '' || $part == '.') {
1317 } elseif ($part == '..') {
1318 if (array_pop($absolutes) === null) {
1319 return false;
1320 }
1321 } else {
1322 $absolutes[] = $part;
1323 }
1324 }
1325 return $target . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $absolutes);
1326 }
1327 }
1328 namespace Kibo\Phast\Retrievers;
1329
1330 class CachingRetriever implements \Kibo\Phast\Retrievers\Retriever
1331 {
1332 use \Kibo\Phast\Retrievers\DynamicCacheSaltTrait {
1333 getCacheSalt as getDynamicCacheSalt;
1334 }
1335 /**
1336 * @var Cache
1337 */
1338 private $cache;
1339 /**
1340 * @var Retriever
1341 */
1342 private $retriever;
1343 /**
1344 * CachingRetriever constructor.
1345 *
1346 * @param Retriever $retriever
1347 * @param Cache $cache
1348 * @param int $defaultCacheTime
1349 */
1350 public function __construct(\Kibo\Phast\Cache\Cache $cache, \Kibo\Phast\Retrievers\Retriever $retriever = null, $defaultCacheTime = 0)
1351 {
1352 $this->cache = $cache;
1353 $this->retriever = $retriever;
1354 }
1355 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
1356 {
1357 if ($this->retriever) {
1358 return $this->getCachedWithRetriever($url);
1359 }
1360 return $this->getFromCacheOnly($url);
1361 }
1362 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
1363 {
1364 if ($this->retriever) {
1365 return $this->retriever->getCacheSalt($url);
1366 }
1367 return $this->getDynamicCacheSalt($url);
1368 }
1369 private function getCachedWithRetriever(\Kibo\Phast\ValueObjects\URL $url)
1370 {
1371 return $this->cache->get($this->getCacheKey($url), function () use($url) {
1372 return $this->retriever->retrieve($url);
1373 });
1374 }
1375 private function getFromCacheOnly(\Kibo\Phast\ValueObjects\URL $url)
1376 {
1377 $cached = $this->cache->get($this->getCacheKey($url));
1378 if (!$cached) {
1379 return false;
1380 }
1381 return $cached;
1382 }
1383 private function getCacheKey(\Kibo\Phast\ValueObjects\URL $url)
1384 {
1385 return $url . '-' . $this->getCacheSalt($url);
1386 }
1387 }
1388 namespace Kibo\Phast\Retrievers;
1389
1390 class PostDataRetriever implements \Kibo\Phast\Retrievers\Retriever
1391 {
1392 /**
1393 * @var ObjectifiedFunctions
1394 */
1395 private $funcs;
1396 private $content;
1397 /**
1398 * PostDataRetriever constructor.
1399 * @param ObjectifiedFunctions $funcs
1400 */
1401 public function __construct(\Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
1402 {
1403 $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs;
1404 }
1405 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
1406 {
1407 if (!isset($this->content)) {
1408 $this->content = $this->funcs->file_get_contents('php://input');
1409 }
1410 return $this->content;
1411 }
1412 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
1413 {
1414 return md5($this->retrieve($url));
1415 }
1416 }
1417 namespace Kibo\Phast\Filters\HTML\Composite;
1418
1419 class Factory
1420 {
1421 use \Kibo\Phast\Logging\LoggingTrait;
1422 public function make(array $config)
1423 {
1424 $composite = new \Kibo\Phast\Filters\HTML\Composite\Filter(\Kibo\Phast\ValueObjects\URL::fromString($config['documents']['baseUrl']), $config['outputServerSideStats']);
1425 foreach (array_keys($config['documents']['filters']) as $class) {
1426 $package = \Kibo\Phast\Environment\Package::fromPackageClass($class);
1427 if ($package->hasFactory()) {
1428 $filter = $package->getFactory()->make($config);
1429 } elseif (!class_exists($class)) {
1430 $this->logger(__METHOD__, __LINE__)->error("Skipping non-existent filter class: {$class}");
1431 continue;
1432 } else {
1433 $filter = new $class();
1434 }
1435 $composite->addHTMLFilter($filter);
1436 }
1437 return $composite;
1438 }
1439 }
1440 namespace Kibo\Phast\Filters\HTML\Composite;
1441
1442 class Filter
1443 {
1444 use \Kibo\Phast\Logging\LoggingTrait;
1445 /**
1446 * @var URL
1447 */
1448 private $baseUrl;
1449 private $outputStats;
1450 /**
1451 * @var HTMLStreamFilter[]
1452 */
1453 private $filters = array();
1454 private $timings = array();
1455 /**
1456 * Filter constructor.
1457 * @param URL $baseUrl
1458 * @param $outputStats
1459 */
1460 public function __construct(\Kibo\Phast\ValueObjects\URL $baseUrl, $outputStats)
1461 {
1462 $this->baseUrl = $baseUrl;
1463 $this->outputStats = $outputStats;
1464 }
1465 /**
1466 * @param string $buffer
1467 * @return string
1468 */
1469 public function apply($buffer)
1470 {
1471 $timeStart = microtime(true);
1472 try {
1473 return $this->tryToApply($buffer, $timeStart);
1474 } catch (\Exception $e) {
1475 $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()]);
1476 return $buffer;
1477 }
1478 }
1479 public function addHTMLFilter(\Kibo\Phast\Filters\HTML\HTMLStreamFilter $filter)
1480 {
1481 $this->filters[] = $filter;
1482 }
1483 private function tryToApply($buffer, $timeStart)
1484 {
1485 $context = new \Kibo\Phast\Filters\HTML\HTMLPageContext($this->baseUrl);
1486 $elements = (new \Kibo\Phast\Parsing\HTML\PCRETokenizer())->tokenize($buffer);
1487 foreach ($this->filters as $filter) {
1488 $this->logger()->info('Starting {filter}', ['filter' => get_class($filter)]);
1489 $elements = $filter->transformElements($elements, $context);
1490 }
1491 $output = '';
1492 foreach ($elements as $element) {
1493 $output .= $element;
1494 }
1495 $timeDelta = microtime(true) - $timeStart;
1496 if ($this->outputStats) {
1497 $output .= sprintf("\n<!-- [Phast] Document optimized in %dms -->\n", $timeDelta * 1000);
1498 }
1499 return $output;
1500 }
1501 public function selectFilters($callback)
1502 {
1503 $this->filters = array_filter($this->filters, $callback);
1504 }
1505 }
1506 namespace Kibo\Phast\Filters\HTML;
1507
1508 interface AMPCompatibleFilter
1509 {
1510 }
1511 namespace Kibo\Phast\Filters\HTML;
1512
1513 interface HTMLStreamFilter
1514 {
1515 /**
1516 * @param \Traversable $elements
1517 * @param HTMLPageContext $context
1518 * @return \Traversable
1519 */
1520 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context);
1521 }
1522 namespace Kibo\Phast\Filters\HTML\MinifyScripts;
1523
1524 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
1525 {
1526 use \Kibo\Phast\Filters\HTML\Helpers\JSDetectorTrait;
1527 private $cache;
1528 public function __construct(\Kibo\Phast\Cache\File\Cache $cache)
1529 {
1530 $this->cache = $cache;
1531 }
1532 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
1533 {
1534 foreach ($elements as $element) {
1535 if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag && $element->getTagName() === 'script' && ($content = $element->getTextContent()) !== '') {
1536 $content = trim($content);
1537 if ($this->isJSElement($element) && preg_match('~[()[\\]{};]\\s~', $content)) {
1538 $content = preg_replace('~^\\s*<!--\\s*\\n(.*)\\n\\s*-->\\s*$~s', '$1', $content);
1539 $content = $this->cache->get(md5($content), function () use($content) {
1540 return (new \Kibo\Phast\Common\JSMinifier($content, true))->min();
1541 });
1542 } elseif (($data = @json_decode($content)) !== null && ($newContent = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) !== false) {
1543 $content = str_replace('</', '<\\/', $newContent);
1544 }
1545 $element->setTextContent($content);
1546 }
1547 (yield $element);
1548 }
1549 }
1550 }
1551 namespace Kibo\Phast\Filters\HTML\MinifyScripts;
1552
1553 class Factory
1554 {
1555 public function make(array $config)
1556 {
1557 return new \Kibo\Phast\Filters\HTML\MinifyScripts\Filter(new \Kibo\Phast\Cache\File\Cache($config['cache'], 'minified-inline-scripts'));
1558 }
1559 }
1560 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService;
1561
1562 class ImageURLRewriterFactory
1563 {
1564 public function make(array $config, $filterClass = '')
1565 {
1566 $signature = (new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config);
1567 if (isset($config['documents']['filters'][$filterClass])) {
1568 $classConfig = $config['documents']['filters'][$filterClass];
1569 } elseif (isset($config['styles']['filters'][$filterClass])) {
1570 $classConfig = $config['styles']['filters'][$filterClass];
1571 } else {
1572 $classConfig = [];
1573 }
1574 if (isset($classConfig['serviceUrl'])) {
1575 $serviceUrl = $classConfig['serviceUrl'];
1576 } else {
1577 $serviceUrl = $config['servicesUrl'] . '?service=images';
1578 }
1579 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']);
1580 }
1581 }
1582 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService;
1583
1584 class ImageInliningManager
1585 {
1586 use \Kibo\Phast\Logging\LoggingTrait;
1587 /**
1588 * @var Cache
1589 */
1590 private $cache;
1591 /**
1592 * @var int
1593 */
1594 private $maxImageInliningSize;
1595 /**
1596 * ImageInliningManager constructor.
1597 * @param Cache $cache
1598 * @param int $maxImageInliningSize
1599 */
1600 public function __construct(\Kibo\Phast\Cache\Cache $cache, $maxImageInliningSize)
1601 {
1602 $this->cache = $cache;
1603 $this->maxImageInliningSize = $maxImageInliningSize;
1604 }
1605 /**
1606 * @return int
1607 */
1608 public function getMaxImageInliningSize()
1609 {
1610 return $this->maxImageInliningSize;
1611 }
1612 /**
1613 * @param Resource $resource
1614 * @return string|null
1615 */
1616 public function getUrlForInlining(\Kibo\Phast\ValueObjects\Resource $resource)
1617 {
1618 if ($resource->getMimeType() !== 'image/svg+xml') {
1619 return $this->cache->get($this->getCacheKey($resource));
1620 }
1621 try {
1622 if ($this->hasSizeForInlining($resource)) {
1623 return $resource->toDataURL();
1624 }
1625 } catch (\Kibo\Phast\Exceptions\ItemNotFoundException $e) {
1626 $this->logger()->warning('Could not fetch contents for {url}. Message is {message}', ['url' => $resource->getUrl()->toString(), 'message' => $e->getMessage()]);
1627 }
1628 return null;
1629 }
1630 public function maybeStoreForInlining(\Kibo\Phast\ValueObjects\Resource $resource)
1631 {
1632 if ($this->shouldStoreForInlining($resource)) {
1633 $this->logger()->info('Storing {url} for inlining', ['url' => $resource->getUrl()->toString()]);
1634 $this->cache->set($this->getCacheKey($resource), $resource->toDataURL());
1635 } else {
1636 $this->logger()->info('Not storing {url} for inlining', ['url' => $resource->getUrl()->toString()]);
1637 }
1638 }
1639 private function shouldStoreForInlining(\Kibo\Phast\ValueObjects\Resource $resource)
1640 {
1641 return $this->hasSizeForInlining($resource) && strpos($resource->getMimeType(), 'image/') === 0 && $resource->getMimeType() !== 'image/webp';
1642 }
1643 private function hasSizeForInlining(\Kibo\Phast\ValueObjects\Resource $resource)
1644 {
1645 $size = $resource->getSize();
1646 return $size !== false && $size <= $this->maxImageInliningSize;
1647 }
1648 private function getCacheKey(\Kibo\Phast\ValueObjects\Resource $resource)
1649 {
1650 return $resource->getUrl()->toString() . '|' . $resource->getCacheSalt() . '|' . $this->maxImageInliningSize;
1651 }
1652 }
1653 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService;
1654
1655 class ImageURLRewriter
1656 {
1657 use \Kibo\Phast\Logging\LoggingTrait;
1658 /**
1659 * @var ServiceSignature
1660 */
1661 protected $signature;
1662 /**
1663 * @var Retriever
1664 */
1665 protected $retriever;
1666 /**
1667 * @var ImageInliningManager
1668 */
1669 protected $inliningManager;
1670 /**
1671 * @var URL
1672 */
1673 protected $baseUrl;
1674 /**
1675 * @var URL
1676 */
1677 protected $serviceUrl;
1678 /**
1679 * @var string[]
1680 */
1681 protected $whitelist;
1682 /**
1683 * @var Resource[]
1684 */
1685 protected $inlinedResources;
1686 /**
1687 * ImageURLRewriter constructor.
1688 * @param ServiceSignature $signature
1689 * @param LocalRetriever $retriever
1690 * @param ImageInliningManager $inliningManager
1691 * @param URL $baseUrl
1692 * @param URL $serviceUrl
1693 * @param array $whitelist
1694 */
1695 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)
1696 {
1697 $this->signature = $signature;
1698 $this->retriever = $retriever;
1699 $this->inliningManager = $inliningManager;
1700 $this->baseUrl = $baseUrl;
1701 $this->serviceUrl = $serviceUrl;
1702 $this->whitelist = $whitelist;
1703 }
1704 /**
1705 * @param string $url
1706 * @param URL|null $baseUrl
1707 * @param array $params
1708 * @param bool $mustExist
1709 * @return string
1710 */
1711 public function rewriteUrl($url, \Kibo\Phast\ValueObjects\URL $baseUrl = null, array $params = array(), $mustExist = false)
1712 {
1713 if (strpos($url, '#') === 0) {
1714 return $url;
1715 }
1716 $this->inlinedResources = [];
1717 $absolute = $this->makeURLAbsoluteToBase($url, $baseUrl);
1718 if (!$this->shouldRewriteUrl($absolute)) {
1719 return $url;
1720 }
1721 $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever($absolute, $this->retriever);
1722 if ($mustExist && $resource->getSize() === false) {
1723 return $url;
1724 }
1725 $dataUrl = $this->inliningManager->getUrlForInlining($resource);
1726 if ($dataUrl) {
1727 $this->inlinedResources = [$resource];
1728 return $dataUrl;
1729 }
1730 $params['src'] = $absolute->toString();
1731 return $this->makeSignedUrl($params);
1732 }
1733 /**
1734 * @param $styleContent
1735 * @return string
1736 */
1737 public function rewriteStyle($styleContent)
1738 {
1739 $allInlined = [];
1740 $result = preg_replace_callback('~
1741 (\\b (?: image | background ):)
1742 ([^;}]*)
1743 ~xiS', function ($match) use(&$allInlined) {
1744 return $match[1] . $this->rewriteStyleRule($match[2], $allInlined);
1745 }, $styleContent);
1746 $this->inlinedResources = array_values($allInlined);
1747 return $result;
1748 }
1749 private function rewriteStyleRule($ruleContent, &$allInlined)
1750 {
1751 return preg_replace_callback('~
1752 ( \\b url \\( [\'"]? )
1753 ( [^\'")] ++ )
1754 ~xiS', function ($match) use(&$allInlined) {
1755 $url = $match[1] . $this->rewriteUrl($match[2]);
1756 if (!empty($this->inlinedResources)) {
1757 $inlined = $this->inlinedResources[0];
1758 $allInlined[$inlined->getUrl()->toString()] = $inlined;
1759 }
1760 return $url;
1761 }, $ruleContent);
1762 }
1763 /**
1764 * @return Resource[]
1765 */
1766 public function getInlinedResources()
1767 {
1768 return $this->inlinedResources;
1769 }
1770 /**
1771 * @return string
1772 */
1773 public function getCacheSalt()
1774 {
1775 $parts = array_merge([$this->signature->getCacheSalt(), $this->baseUrl->toString(), $this->serviceUrl->toString(), $this->inliningManager->getMaxImageInliningSize(), '20180413'], array_keys($this->whitelist), array_values($this->whitelist));
1776 return join('-', $parts);
1777 }
1778 /**
1779 * @param string $url
1780 * @param URL|null $baseUrl
1781 * @return URL
1782 */
1783 private function makeURLAbsoluteToBase($url, \Kibo\Phast\ValueObjects\URL $baseUrl = null)
1784 {
1785 $url = trim($url);
1786 if (!$url || substr($url, 0, 5) === 'data:') {
1787 return null;
1788 }
1789 $this->logger()->info('Rewriting img {url}', ['url' => $url]);
1790 $baseUrl = is_null($baseUrl) ? $this->baseUrl : $baseUrl;
1791 return \Kibo\Phast\ValueObjects\URL::fromString($url)->withBase($baseUrl);
1792 }
1793 /**
1794 * @param string $url
1795 * @return bool
1796 */
1797 private function shouldRewriteUrl($url)
1798 {
1799 if (!$url) {
1800 return false;
1801 }
1802 foreach ($this->whitelist as $pattern) {
1803 if (preg_match($pattern, $url)) {
1804 return true;
1805 }
1806 }
1807 $urlObject = \Kibo\Phast\ValueObjects\URL::fromString($url);
1808 if (preg_match('~\\.(jpe?g|gif|png)$~i', $urlObject->getPath()) && $this->retriever->getCacheSalt($urlObject)) {
1809 return true;
1810 }
1811 return false;
1812 }
1813 /**
1814 * @param array $params
1815 * @return string
1816 */
1817 private function makeSignedUrl(array $params)
1818 {
1819 $params['cacheMarker'] = $this->retriever->getCacheSalt(\Kibo\Phast\ValueObjects\URL::fromString($params['src']));
1820 return (new \Kibo\Phast\Services\ServiceRequest())->withParams($params)->withUrl($this->serviceUrl)->sign($this->signature)->serialize();
1821 }
1822 }
1823 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags;
1824
1825 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter, \Kibo\Phast\Filters\HTML\AMPCompatibleFilter
1826 {
1827 const IMG_SRC_ATTR_PATTERN = '~^(|data-(|lazy-|wood-))src$~i';
1828 const IMG_SRCSET_ATTR_PATTERN = '~^(|data-(|lazy-|wood-))srcset$~i';
1829 /**
1830 * @var ImageURLRewriter
1831 */
1832 private $rewriter;
1833 private $inPictureTag = false;
1834 private $inBody = false;
1835 private $imagePathPattern;
1836 /**
1837 * Filter constructor.
1838 * @param ImageURLRewriter $rewriter
1839 */
1840 public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter)
1841 {
1842 $this->rewriter = $rewriter;
1843 $this->imagePathPattern = $this->makeImagePathPattern();
1844 }
1845 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
1846 {
1847 foreach ($elements as $element) {
1848 if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag) {
1849 $this->handleTag($element, $context);
1850 } elseif ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag) {
1851 $this->handleClosingTag($element);
1852 }
1853 (yield $element);
1854 }
1855 }
1856 private function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
1857 {
1858 $isImage = false;
1859 if ($tag->getTagName() == 'img' || $this->inPictureTag && $tag->getTagName() == 'source' || $tag->getTagName() == 'amp-img') {
1860 $isImage = true;
1861 } elseif ($tag->getTagName() == 'picture') {
1862 $this->inPictureTag = true;
1863 } elseif ($tag->getTagName() == 'video' || $tag->getTagName() == 'audio') {
1864 $this->inPictureTag = false;
1865 } elseif ($tag->getTagName() == 'body') {
1866 $this->inBody = true;
1867 } elseif ($tag->getTagName() == 'meta') {
1868 return;
1869 }
1870 foreach ($tag->getAttributes() as $k => $v) {
1871 if (!$v) {
1872 continue;
1873 }
1874 if ($isImage && preg_match(self::IMG_SRC_ATTR_PATTERN, $k)) {
1875 $this->rewriteSrc($tag, $context, $k);
1876 } elseif ($isImage && preg_match(self::IMG_SRCSET_ATTR_PATTERN, $k)) {
1877 $this->rewriteSrcset($tag, $context, $k);
1878 } elseif ($this->inBody && preg_match($this->imagePathPattern, parse_url($v, PHP_URL_PATH))) {
1879 $this->rewriteArbitraryAttribute($tag, $context, $k);
1880 }
1881 }
1882 }
1883 private function makeImagePathPattern()
1884 {
1885 $pieces = [];
1886 foreach (\Kibo\Phast\ValueObjects\Resource::EXTENSION_TO_MIME_TYPE as $ext => $mime) {
1887 if (strpos($mime, 'image/') === 0) {
1888 $pieces[] = preg_quote($ext, '~');
1889 }
1890 }
1891 return '~\\.(?:' . implode('|', $pieces) . ')$~';
1892 }
1893 private function handleClosingTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag $closingTag)
1894 {
1895 if ($closingTag->getTagName() == 'picture') {
1896 $this->inPictureTag = false;
1897 }
1898 }
1899 private function rewriteSrc(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $img, \Kibo\Phast\Filters\HTML\HTMLPageContext $context, $attribute)
1900 {
1901 $url = $img->getAttribute($attribute);
1902 $newURL = $this->rewriter->rewriteUrl($url, $context->getBaseUrl());
1903 $img->setAttribute($attribute, $newURL);
1904 }
1905 private function rewriteSrcset(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $img, \Kibo\Phast\Filters\HTML\HTMLPageContext $context, $attribute)
1906 {
1907 $srcset = $img->getAttribute($attribute);
1908 $rewritten = preg_replace_callback('/([^,\\s]+)(\\s+(?:[^,]+))?/', function ($match) use($context) {
1909 $url = $this->rewriter->rewriteUrl($match[1], $context->getBaseUrl());
1910 if (isset($match[2])) {
1911 return $url . $match[2];
1912 }
1913 return $url;
1914 }, $srcset);
1915 $img->setAttribute($attribute, $rewritten);
1916 }
1917 private function rewriteArbitraryAttribute(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $element, \Kibo\Phast\Filters\HTML\HTMLPageContext $context, $attribute)
1918 {
1919 $url = $element->getAttribute($attribute);
1920 $newUrl = $this->rewriter->rewriteUrl($url, $context->getBaseUrl(), [], true);
1921 $element->setAttribute($attribute, $newUrl);
1922 }
1923 }
1924 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService;
1925
1926 class ImageInliningManagerFactory
1927 {
1928 public function make(array $config)
1929 {
1930 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'inline-images-1');
1931 return new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManager($cache, $config['images']['maxImageInliningSize']);
1932 }
1933 }
1934 namespace Kibo\Phast\Filters\HTML;
1935
1936 class HTMLPageContext
1937 {
1938 /**
1939 * @var URL
1940 */
1941 private $baseUrl;
1942 /**
1943 * @var PhastJavaScript[]
1944 */
1945 private $phastJavaScripts = array();
1946 /**
1947 * HTMLPageContext constructor.
1948 * @param URL $baseUrl
1949 */
1950 public function __construct(\Kibo\Phast\ValueObjects\URL $baseUrl)
1951 {
1952 $this->baseUrl = $baseUrl;
1953 }
1954 /**
1955 * @param URL $baseUrl
1956 */
1957 public function setBaseUrl(\Kibo\Phast\ValueObjects\URL $baseUrl)
1958 {
1959 $this->baseUrl = $baseUrl;
1960 }
1961 /**
1962 * @return URL
1963 */
1964 public function getBaseUrl()
1965 {
1966 return $this->baseUrl;
1967 }
1968 /**
1969 * @param PhastJavaScript $script
1970 */
1971 public function addPhastJavascript(\Kibo\Phast\ValueObjects\PhastJavaScript $script)
1972 {
1973 $this->phastJavaScripts[] = $script;
1974 }
1975 /**
1976 * @return PhastJavaScript[]
1977 */
1978 public function getPhastJavaScripts()
1979 {
1980 return $this->phastJavaScripts;
1981 }
1982 }
1983 namespace Kibo\Phast\Filters\HTML\Helpers;
1984
1985 trait JSDetectorTrait
1986 {
1987 /**
1988 * @param Tag $element
1989 * @return bool
1990 */
1991 private function isJSElement(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $element)
1992 {
1993 if (!$element->hasAttribute('type')) {
1994 return true;
1995 }
1996 return (bool) preg_match('~^(text|application)/javascript(;|$)~i', $element->getAttribute('type'));
1997 }
1998 }
1999 namespace Kibo\Phast\Filters\HTML;
2000
2001 abstract class BaseHTMLStreamFilter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
2002 {
2003 /**
2004 * @var HTMLPageContext
2005 */
2006 protected $context;
2007 /**
2008 * @var \Traversable
2009 */
2010 protected $elements;
2011 /**
2012 * @param Tag $tag
2013 * @return Element[]|\Generator
2014 */
2015 protected abstract function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag);
2016 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
2017 {
2018 $this->context = $context;
2019 $this->elements = $elements;
2020 $this->beforeLoop();
2021 foreach ($this->elements as $element) {
2022 if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag && $this->isTagOfInterest($element)) {
2023 foreach ($this->handleTag($element) as $item) {
2024 (yield $item);
2025 }
2026 } else {
2027 (yield $element);
2028 }
2029 }
2030 $this->afterLoop();
2031 }
2032 /**
2033 * @param Tag $tag
2034 * @return bool
2035 */
2036 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2037 {
2038 return true;
2039 }
2040 protected function beforeLoop()
2041 {
2042 }
2043 protected function afterLoop()
2044 {
2045 }
2046 }
2047 namespace Kibo\Phast\Filters\HTML\PhastScriptsCompiler;
2048
2049 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
2050 {
2051 /**
2052 * @var PhastJavaScriptCompiler
2053 */
2054 private $compiler;
2055 /**
2056 * Filter constructor.
2057 * @param PhastJavaScriptCompiler $compiler
2058 */
2059 public function __construct(\Kibo\Phast\Filters\HTML\PhastScriptsCompiler\PhastJavaScriptCompiler $compiler)
2060 {
2061 $this->compiler = $compiler;
2062 }
2063 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
2064 {
2065 $buffered = [];
2066 $buffering = false;
2067 foreach ($elements as $element) {
2068 if ($this->isClosingBodyTag($element)) {
2069 if ($buffering) {
2070 foreach ($buffered as $bufElement) {
2071 (yield $bufElement);
2072 }
2073 $buffered = [];
2074 }
2075 $buffering = true;
2076 }
2077 if ($buffering) {
2078 $buffered[] = $element;
2079 } else {
2080 (yield $element);
2081 }
2082 }
2083 $scripts = $context->getPhastJavaScripts();
2084 if (!empty($scripts)) {
2085 (yield $this->compileScript($scripts));
2086 }
2087 foreach ($buffered as $element) {
2088 (yield $element);
2089 }
2090 }
2091 /**
2092 * @param PhastJavaScript[] $scripts
2093 * @return Tag
2094 */
2095 private function compileScript(array $scripts)
2096 {
2097 $names = array_map(function (\Kibo\Phast\ValueObjects\PhastJavaScript $script) {
2098 $matches = [];
2099 preg_match('~[^/]*?\\/?[^/]+$~', $script->getFilename(), $matches);
2100 return $matches[0];
2101 }, $scripts);
2102 $script = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag('script');
2103 $script->setAttribute('data-phast-compiled-js-names', join(',', $names));
2104 $compiled = $this->compiler->compileScriptsWithConfig($scripts);
2105 $script->setTextContent($compiled);
2106 return $script;
2107 }
2108 private function isClosingBodyTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element $element)
2109 {
2110 return $element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag && $element->getTagName() == 'body';
2111 }
2112 }
2113 namespace Kibo\Phast\Filters\HTML\PhastScriptsCompiler;
2114
2115 class PhastJavaScriptCompiler
2116 {
2117 /**
2118 * @var Cache
2119 */
2120 private $cache;
2121 /**
2122 * @var string
2123 */
2124 private $serviceUrl;
2125 private $serviceRequestFormat;
2126 /**
2127 * @var \stdClass
2128 */
2129 private $lastCompiledConfig;
2130 /**
2131 * PhastJavaScriptCompiler constructor.
2132 * @param Cache $cache
2133 * @param string $serviceUrl
2134 */
2135 public function __construct(\Kibo\Phast\Cache\Cache $cache, $serviceUrl, $serviceRequestFormat)
2136 {
2137 $this->cache = $cache;
2138 $this->serviceUrl = (new \Kibo\Phast\Services\ServiceRequest())->withUrl(\Kibo\Phast\ValueObjects\URL::fromString((string) $serviceUrl))->serialize(\Kibo\Phast\Services\ServiceRequest::FORMAT_QUERY);
2139 $this->serviceRequestFormat = $serviceRequestFormat;
2140 }
2141 /**
2142 * @return \stdClass|null
2143 */
2144 public function getLastCompiledConfig()
2145 {
2146 return $this->lastCompiledConfig;
2147 }
2148 /**
2149 * @param PhastJavaScript[] $scripts
2150 * @return string
2151 */
2152 public function compileScripts(array $scripts)
2153 {
2154 return $this->cache->get($this->getCacheKey($scripts), function () use($scripts) {
2155 return $this->performCompilation($scripts);
2156 });
2157 }
2158 /**
2159 * @param PhastJavaScript[] $scripts
2160 * @return string
2161 */
2162 public function compileScriptsWithConfig(array $scripts)
2163 {
2164 $bundlerMappings = \Kibo\Phast\Services\Bundler\ShortBundlerParamsParser::getParamsMappings();
2165 $jsMappings = array_combine(array_values($bundlerMappings), array_keys($bundlerMappings));
2166 $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");
2167 $resourcesLoader->setConfig('resourcesLoader', ['serviceUrl' => (string) $this->serviceUrl, 'shortParamsMappings' => $jsMappings, 'pathInfo' => $this->serviceRequestFormat === \Kibo\Phast\Services\ServiceRequest::FORMAT_PATH]);
2168 $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,\"-\");var i=j(h+\".q.js\");return f.replace(/\\?.*\$/,\"\").replace(/\\/__p__\\.js\$/,\"\")+\"/\"+i;function j(l){return k(k(l).match(/[\\s\\S]{1,255}/g).join(\"/\"))}function k(m){return m.split(\"\").reverse().join(\"\")}}function appendQueryString(n,o){var p=n.indexOf(\"?\")>-1?\"&\":\"?\";return n+p+o}\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);
2169 $compiled = $this->compileScripts($scripts);
2170 return '(' . $compiled . ')(' . $this->compileConfig($scripts) . ');';
2171 }
2172 /**
2173 * @param PhastJavaScript[] $scripts
2174 * @return string
2175 */
2176 private function performCompilation(array $scripts)
2177 {
2178 $compiled = implode(',', array_map(function (\Kibo\Phast\ValueObjects\PhastJavaScript $script) {
2179 return $this->interpolate($script->getContents());
2180 }, $scripts));
2181 return 'function phastScripts(phast){phast.scripts=[' . $compiled . '];(phast.scripts.shift())();}';
2182 }
2183 /**
2184 * @param PhastJavaScript[] $scripts
2185 * @return string
2186 */
2187 private function compileConfig(array $scripts)
2188 {
2189 $config = new \stdClass();
2190 foreach ($scripts as $script) {
2191 if ($script->hasConfig()) {
2192 $config->{$script->getConfigKey()} = $script->getConfig();
2193 }
2194 }
2195 $this->lastCompiledConfig = $config;
2196 return \Kibo\Phast\Common\JSON::encode(['config' => base64_encode(\Kibo\Phast\Common\JSON::encode($config))]);
2197 }
2198 /**
2199 * @param string $script
2200 * @return string
2201 */
2202 private function interpolate($script)
2203 {
2204 return sprintf('(function(){%s})', $script);
2205 }
2206 /**
2207 * @param PhastJavaScript[] $scripts
2208 * @return string
2209 */
2210 private function getCacheKey(array $scripts)
2211 {
2212 return array_reduce($scripts, function ($carry, \Kibo\Phast\ValueObjects\PhastJavaScript $script) {
2213 $carry .= $script->getFilename() . '-' . $script->getCacheSalt() . "\n";
2214 return $carry;
2215 }, '');
2216 }
2217 }
2218 namespace Kibo\Phast\Filters\HTML\LazyImageLoading;
2219
2220 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
2221 {
2222 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2223 {
2224 if (!$tag->hasAttribute('loading')) {
2225 $tag->setAttribute('loading', 'lazy');
2226 }
2227 (yield $tag);
2228 }
2229 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2230 {
2231 return $tag->getTagName() == 'img';
2232 }
2233 }
2234 namespace Kibo\Phast\Filters\HTML\ScriptsProxyService;
2235
2236 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
2237 {
2238 use \Kibo\Phast\Filters\HTML\Helpers\JSDetectorTrait, \Kibo\Phast\Logging\LoggingTrait;
2239 /**
2240 * @var array
2241 */
2242 private $config;
2243 /**
2244 * @var ServiceSignature
2245 */
2246 private $signature;
2247 /**
2248 * @var LocalRetriever
2249 */
2250 private $retriever;
2251 private $tokenRefMaker;
2252 /**
2253 * @var ObjectifiedFunctions
2254 */
2255 private $functions;
2256 /**
2257 * @var bool
2258 */
2259 private $didInject = false;
2260 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)
2261 {
2262 $this->config = $config;
2263 $this->signature = $signature;
2264 $this->retriever = $retriever;
2265 $this->tokenRefMaker = $tokenRefMaker;
2266 $this->functions = is_null($functions) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $functions;
2267 }
2268 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2269 {
2270 return $tag->getTagName() == 'script' && $this->isJSElement($tag);
2271 }
2272 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
2273 {
2274 $this->rewriteScriptSource($script);
2275 if (!$this->didInject) {
2276 $this->addScript();
2277 $this->didInject = true;
2278 }
2279 (yield $script);
2280 }
2281 private function rewriteScriptSource(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $element)
2282 {
2283 if (!$element->hasAttribute('src')) {
2284 return;
2285 }
2286 $src = trim($element->getAttribute('src'));
2287 $url = $this->getAbsoluteURL($src);
2288 $cacheMarker = $this->retriever->getCacheSalt($url);
2289 if (!$cacheMarker) {
2290 return;
2291 }
2292 $cacheMarker .= '-' . \Kibo\Phast\Filters\JavaScript\Minification\JSMinifierFilter::VERSION;
2293 $element->setAttribute('src', $this->makeProxiedURL($url, $cacheMarker));
2294 $element->setAttribute('data-phast-original-src', (string) $url);
2295 $element->setAttribute('data-phast-params', $this->makeServiceParams($url, $cacheMarker));
2296 }
2297 private function makeProxiedURL(\Kibo\Phast\ValueObjects\URL $url, $cacheMarker)
2298 {
2299 $params = ['service' => 'scripts', 'src' => (string) $url->withoutQuery(), 'cacheMarker' => $cacheMarker];
2300 return (new \Kibo\Phast\Services\ServiceRequest())->withUrl(\Kibo\Phast\ValueObjects\URL::fromString($this->config['serviceUrl']))->withParams($params)->serialize();
2301 }
2302 private function makeServiceParams(\Kibo\Phast\ValueObjects\URL $url, $cacheMarker)
2303 {
2304 return \Kibo\Phast\Services\Bundler\ServiceParams::fromArray(['src' => (string) $url->withoutQuery(), 'cacheMarker' => $cacheMarker, 'isScript' => '1'])->sign($this->signature)->replaceByTokenRef($this->tokenRefMaker)->serialize();
2305 }
2306 private function addScript()
2307 {
2308 $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']];
2309 $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");
2310 $script->setConfig('script-proxy-service', $config);
2311 $this->context->addPhastJavaScript($script);
2312 }
2313 private function getAbsoluteURL($url)
2314 {
2315 return \Kibo\Phast\ValueObjects\URL::fromString($url)->withBase($this->context->getBaseUrl());
2316 }
2317 }
2318 namespace Kibo\Phast\Filters\HTML\MetaCharset;
2319
2320 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
2321 {
2322 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
2323 {
2324 $didYield = false;
2325 foreach ($elements as $element) {
2326 if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag) {
2327 if ($element->tagName == 'meta' && array_keys($element->getAttributes()) == ['charset']) {
2328 continue;
2329 }
2330 if (!$didYield && !in_array($element->tagName, ['html', 'head'])) {
2331 (yield new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag('meta', ['charset' => 'utf-8']));
2332 $didYield = true;
2333 }
2334 }
2335 (yield $element);
2336 }
2337 }
2338 }
2339 namespace Kibo\Phast\Filters\HTML\BaseURLSetter;
2340
2341 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
2342 {
2343 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2344 {
2345 return $tag->getTagName() == 'base' && $tag->hasAttribute('href');
2346 }
2347 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2348 {
2349 $base = \Kibo\Phast\ValueObjects\URL::fromString($tag->getAttribute('href'));
2350 $current = $this->context->getBaseUrl();
2351 $this->context->setBaseUrl($base->withBase($current));
2352 (yield $tag);
2353 }
2354 }
2355 namespace Kibo\Phast\Filters\HTML\CSSInlining;
2356
2357 class OptimizerFactory
2358 {
2359 /**
2360 * @var Cache
2361 */
2362 private $cache;
2363 public function __construct(array $config)
2364 {
2365 $this->cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'css-optimizitor');
2366 }
2367 /**
2368 * @param \Traversable $elements
2369 * @return Optimizer
2370 */
2371 public function makeForElements(\Traversable $elements)
2372 {
2373 return new \Kibo\Phast\Filters\HTML\CSSInlining\Optimizer($elements, $this->cache);
2374 }
2375 }
2376 namespace Kibo\Phast\Filters\HTML\CSSInlining;
2377
2378 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
2379 {
2380 use \Kibo\Phast\Logging\LoggingTrait;
2381 const CSS_IMPORTS_REGEXP = '~
2382 @import \\s++
2383 ( url \\( )?+ # url() is optional
2384 ( (?(1) ["\']?+ | ["\'] ) ) # without url() a quote is necessary
2385 \\s*+ (?<url>[A-Za-z0-9_/.:?&=+%,-]++) \\s*+
2386 \\2 # match ending quote
2387 (?(1)\\)) # match closing paren if url( was used
2388 \\s*+ ;
2389 ~xi';
2390 /**
2391 * @var ServiceSignature
2392 */
2393 private $signature;
2394 /**
2395 * @var int
2396 */
2397 private $maxInlineDepth = 2;
2398 /**
2399 * @var URL
2400 */
2401 private $baseURL;
2402 /**
2403 * @var string[]
2404 */
2405 private $whitelist = array();
2406 /**
2407 * @var string
2408 */
2409 private $serviceUrl;
2410 /**
2411 * @var int
2412 */
2413 private $optimizerSizeDiffThreshold;
2414 /**
2415 * @var Retriever
2416 */
2417 private $localRetriever;
2418 /**
2419 * @var Retriever
2420 */
2421 private $retriever;
2422 /**
2423 * @var OptimizerFactory
2424 */
2425 private $optimizerFactory;
2426 /**
2427 * @var ServiceFilter
2428 */
2429 private $cssFilter;
2430 /**
2431 * @var Optimizer
2432 */
2433 private $optimizer;
2434 /**
2435 * @var TokenRefMaker
2436 */
2437 private $tokenRefMaker;
2438 /**
2439 * @var string[]
2440 */
2441 private $cacheMarkers = array();
2442 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)
2443 {
2444 $this->signature = $signature;
2445 $this->baseURL = $baseURL;
2446 $this->serviceUrl = \Kibo\Phast\ValueObjects\URL::fromString((string) $config['serviceUrl']);
2447 $this->optimizerSizeDiffThreshold = (int) $config['optimizerSizeDiffThreshold'];
2448 $this->localRetriever = $localRetriever;
2449 $this->retriever = $retriever;
2450 $this->optimizerFactory = $optimizerFactory;
2451 $this->cssFilter = $cssFilter;
2452 $this->tokenRefMaker = $tokenRefMaker;
2453 foreach ($config['whitelist'] as $key => $value) {
2454 if (!is_array($value)) {
2455 $this->whitelist[$value] = ['ieCompatible' => true];
2456 $key = $value;
2457 } else {
2458 $this->whitelist[$key] = $value;
2459 }
2460 }
2461 }
2462 protected function beforeLoop()
2463 {
2464 $this->elements = iterator_to_array($this->elements);
2465 $this->optimizer = $this->optimizerFactory->makeForElements(new \ArrayIterator($this->elements));
2466 }
2467 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2468 {
2469 return $tag->getTagName() == 'style' || $tag->getTagName() == 'link' && $tag->getAttribute('rel') == 'stylesheet' && $tag->hasAttribute('href');
2470 }
2471 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2472 {
2473 if ($tag->getTagName() == 'link') {
2474 return $this->inlineLink($tag, $this->context->getBaseUrl());
2475 }
2476 return $this->inlineStyle($tag);
2477 }
2478 protected function afterLoop()
2479 {
2480 $this->addIEFallbackScript();
2481 $this->addInlinedRetrieverScript();
2482 }
2483 private function inlineLink(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $link, \Kibo\Phast\ValueObjects\URL $baseUrl)
2484 {
2485 $href = trim($link->getAttribute('href'));
2486 if (trim($href, '/') == '') {
2487 return [$link];
2488 }
2489 $location = \Kibo\Phast\ValueObjects\URL::fromString($href)->withBase($baseUrl);
2490 if (!$this->findInWhitelist($location) && !$this->localRetriever->getCacheSalt($location)) {
2491 return [$link];
2492 }
2493 $media = $link->getAttribute('media');
2494 if (preg_match('~^\\s*(this\\.)?media\\s*=\\s*(?<q>[\'"])(?<m>((?!\\k<q>).)+?)\\k<q>\\s*(;|$)~', $link->getAttribute('onload'), $match)) {
2495 $media = $match['m'];
2496 }
2497 $elements = $this->inlineURL($location, $media);
2498 return is_null($elements) ? [$link] : $elements;
2499 }
2500 private function inlineStyle(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $style)
2501 {
2502 $processed = $this->cssFilter->apply(\Kibo\Phast\ValueObjects\Resource::makeWithContent($this->baseURL, $style->textContent), [])->getContent();
2503 $elements = $this->inlineCSS($this->baseURL, $processed, $style->getAttribute('media'), false);
2504 if (($id = $style->getAttribute('id')) != '') {
2505 if (sizeof($elements) == 1) {
2506 $elements[0]->setAttribute('id', $id);
2507 } else {
2508 foreach ($elements as $element) {
2509 $element->setAttribute('data-phast-original-id', $id);
2510 }
2511 }
2512 }
2513 return $elements;
2514 }
2515 private function findInWhitelist(\Kibo\Phast\ValueObjects\URL $url)
2516 {
2517 $stringUrl = (string) $url;
2518 foreach ($this->whitelist as $pattern => $settings) {
2519 if (preg_match($pattern, $stringUrl)) {
2520 return $settings;
2521 }
2522 }
2523 return false;
2524 }
2525 /**
2526 * @param URL $url
2527 * @param string $media
2528 * @param boolean $ieCompatible
2529 * @param int $currentLevel
2530 * @param string[] $seen
2531 * @return Tag[]|null
2532 * @throws \Kibo\Phast\Exceptions\ItemNotFoundException
2533 */
2534 private function inlineURL(\Kibo\Phast\ValueObjects\URL $url, $media, $ieCompatible = true, $currentLevel = 0, $seen = array())
2535 {
2536 $whitelistEntry = $this->findInWhitelist($url);
2537 if (!$whitelistEntry) {
2538 $whitelistEntry = !!$this->localRetriever->getCacheSalt($url);
2539 }
2540 if (!$whitelistEntry) {
2541 $this->logger()->info('Not inlining {url}. Not in whitelist', ['url' => $url]);
2542 return [$this->makeLink($url, $media)];
2543 }
2544 if (isset($whitelistEntry['ieCompatible']) && !$whitelistEntry['ieCompatible']) {
2545 $ieFallbackUrl = $ieCompatible ? $url : null;
2546 $ieCompatible = false;
2547 } else {
2548 $ieFallbackUrl = null;
2549 }
2550 if (in_array($url, $seen)) {
2551 return [];
2552 }
2553 if ($currentLevel > $this->maxInlineDepth) {
2554 return $this->addIEFallback($ieFallbackUrl, [$this->makeLink($url, $media)]);
2555 }
2556 $seen[] = $url;
2557 $this->logger()->info('Inlining {url}.', ['url' => (string) $url]);
2558 $content = $this->retriever->retrieve($url);
2559 if ($content === false) {
2560 return $this->addIEFallback($ieFallbackUrl, [$this->makeServiceLink($url, $media)]);
2561 }
2562 $content = $this->cssFilter->apply(\Kibo\Phast\ValueObjects\Resource::makeWithContent($url, $content), [])->getContent();
2563 $this->cacheMarkers[$url->toString()] = \Kibo\Phast\Common\Base64url::shortHash(implode("\0", [$this->retriever->getCacheSalt($url), $content]));
2564 $optimized = $this->optimizer->optimizeCSS($content);
2565 if ($optimized === null) {
2566 $this->logger()->error('CSS optimizer failed for {url}', ['url' => (string) $url]);
2567 return null;
2568 }
2569 $isOptimized = false;
2570 if (strlen($content) - strlen($optimized) > $this->optimizerSizeDiffThreshold) {
2571 $content = $optimized;
2572 $isOptimized = true;
2573 }
2574 $elements = $this->inlineCSS($url, $content, $media, $isOptimized, $ieCompatible, $currentLevel, $seen);
2575 $this->addIEFallback($ieFallbackUrl, $elements);
2576 return $elements;
2577 }
2578 private function inlineCSS(\Kibo\Phast\ValueObjects\URL $url, $content, $media, $optimized, $ieCompatible = true, $currentLevel = 0, $seen = array())
2579 {
2580 $urlMatches = $this->getImportedURLs($content);
2581 $elements = [];
2582 foreach ($urlMatches as $match) {
2583 $matchedUrl = \Kibo\Phast\ValueObjects\URL::fromString($match['url'])->withBase($url);
2584 $replacement = $this->inlineURL($matchedUrl, $media, $ieCompatible, $currentLevel + 1, $seen);
2585 if ($replacement !== null) {
2586 $content = str_replace($match[0], '', $content);
2587 $elements = array_merge($elements, $replacement);
2588 }
2589 }
2590 $elements[] = $this->makeStyle($url, $content, $media, $optimized);
2591 return $elements;
2592 }
2593 private function addIEFallback(\Kibo\Phast\ValueObjects\URL $fallbackUrl = null, array $elements = null)
2594 {
2595 if ($fallbackUrl === null || !$elements) {
2596 return $elements;
2597 }
2598 foreach ($elements as $element) {
2599 $element->setAttribute('data-phast-nested-inlined', '');
2600 }
2601 $element->setAttribute('data-phast-ie-fallback-url', (string) $fallbackUrl);
2602 $element->removeAttribute('data-phast-nested-inlined');
2603 $this->logger()->info('Set {url} as IE fallback URL', ['url' => (string) $fallbackUrl]);
2604 return $elements;
2605 }
2606 private function addIEFallbackScript()
2607 {
2608 $this->logger()->info('Adding IE fallback script');
2609 $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"));
2610 }
2611 private function addInlinedRetrieverScript()
2612 {
2613 $this->logger()->info('Adding inlined retriever script');
2614 $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"));
2615 }
2616 private function getImportedURLs($cssContent)
2617 {
2618 preg_match_all(self::CSS_IMPORTS_REGEXP, $cssContent, $matches, PREG_SET_ORDER);
2619 return $matches;
2620 }
2621 private function makeStyle(\Kibo\Phast\ValueObjects\URL $url, $content, $media, $optimized, $stripImports = true)
2622 {
2623 $style = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag('style');
2624 if ($media !== '' && $media !== 'all') {
2625 $style->setAttribute('media', $media);
2626 }
2627 if ($optimized) {
2628 $style->setAttribute('data-phast-original-src', $url->toString());
2629 $style->setAttribute('data-phast-params', $this->makeServiceParams($url, $stripImports));
2630 }
2631 $content = preg_replace('~(</)(style)~i', '$1 $2', $content);
2632 $style->setTextContent($content);
2633 return $style;
2634 }
2635 private function makeLink(\Kibo\Phast\ValueObjects\URL $url, $media)
2636 {
2637 $link = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag('link', ['rel' => 'stylesheet', 'href' => (string) $url]);
2638 if ($media !== '') {
2639 $link->setAttribute('media', $media);
2640 }
2641 return $link;
2642 }
2643 private function makeServiceLink(\Kibo\Phast\ValueObjects\URL $location, $media)
2644 {
2645 $url = $this->makeServiceURL($location);
2646 return $this->makeLink(\Kibo\Phast\ValueObjects\URL::fromString($url), $media);
2647 }
2648 protected function makeServiceParams(\Kibo\Phast\ValueObjects\URL $originalLocation, $stripImports = false)
2649 {
2650 if (isset($this->cacheMarkers[$originalLocation->toString()])) {
2651 $cacheMarker = $this->cacheMarkers[$originalLocation->toString()];
2652 } else {
2653 $cacheMarker = $this->retriever->getCacheSalt($originalLocation);
2654 }
2655 $src = $originalLocation;
2656 if ($this->localRetriever->getCacheSalt($src)) {
2657 $src = $originalLocation->withoutQuery();
2658 }
2659 $params = ['src' => (string) $src, 'cacheMarker' => $cacheMarker];
2660 if ($stripImports) {
2661 $params['strip-imports'] = 1;
2662 }
2663 return \Kibo\Phast\Services\Bundler\ServiceParams::fromArray($params)->sign($this->signature)->replaceByTokenRef($this->tokenRefMaker)->serialize();
2664 }
2665 protected function makeServiceURL(\Kibo\Phast\ValueObjects\URL $originalLocation)
2666 {
2667 $params = ['service' => 'css', 'src' => (string) $originalLocation, 'cacheMarker' => $this->retriever->getCacheSalt($originalLocation)];
2668 return (new \Kibo\Phast\Services\ServiceRequest())->withUrl($this->serviceUrl)->withParams($params)->sign($this->signature)->serialize();
2669 }
2670 }
2671 namespace Kibo\Phast\Filters\HTML\CSSInlining;
2672
2673 class Optimizer
2674 {
2675 private $classNamePattern = '-?[_a-zA-Z]++[_a-zA-Z0-9-]*+';
2676 /**
2677 * @var array
2678 */
2679 private $usedClasses;
2680 /**
2681 * @var Cache
2682 */
2683 private $cache;
2684 public function __construct(\Traversable $elements, \Kibo\Phast\Cache\Cache $cache)
2685 {
2686 $this->usedClasses = $this->getUsedClasses($elements);
2687 $this->cache = $cache;
2688 }
2689 public function optimizeCSS($css)
2690 {
2691 $stylesheet = $this->cache->get(md5($css), function () use($css) {
2692 return $this->parseCSS($css);
2693 });
2694 if ($stylesheet === null) {
2695 return;
2696 }
2697 $output = '';
2698 $selectors = null;
2699 foreach ($stylesheet as $element) {
2700 if (is_array($element)) {
2701 if ($selectors === null) {
2702 $selectors = [];
2703 }
2704 foreach ($element as $i => $class) {
2705 if ($i !== 0 && !isset($this->usedClasses[$class])) {
2706 continue 2;
2707 }
2708 }
2709 $selectors[] = $element[0];
2710 } elseif ($selectors !== null) {
2711 if (isset($selectors[0])) {
2712 $output .= implode(',', $selectors) . $element;
2713 }
2714 $selectors = null;
2715 } else {
2716 $output .= $element;
2717 }
2718 }
2719 $output = $this->removeEmptyMediaQueries($output);
2720 return trim($output);
2721 }
2722 /**
2723 * Parse a stylesheet into an array of segments
2724 *
2725 * Each string segment is preceded by zero or more arrays encoding selectors
2726 * parsed by parseSelector (see below).
2727 *
2728 * @param $css
2729 * @return array|void
2730 */
2731 private function parseCSS($css)
2732 {
2733 $re_simple_selector_chars = "[A-Z0-9_.#*:>+\\~\\s-]";
2734 $re_selector = "(?: {$re_simple_selector_chars} | \\[[a-z]++\\] )++";
2735 $re_rule = "~\n (?<= ^ | [;{}] ) \\s*+\n ( (?: {$re_selector} , )*+ {$re_selector} )\n ( { [^}]*+ } )\n ~xi";
2736 if (preg_match_all($re_rule, $css, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE) === false) {
2737 // This is an error condition
2738 return;
2739 }
2740 $offset = 0;
2741 $stylesheet = [];
2742 foreach ($matches as $match) {
2743 $selectors = $this->parseSelectors($match[1][0]);
2744 if ($selectors === null) {
2745 continue;
2746 }
2747 if ($match[0][1] > $offset) {
2748 $stylesheet[] = substr($css, $offset, $match[0][1] - $offset);
2749 }
2750 foreach ($selectors as $selector) {
2751 $stylesheet[] = $selector;
2752 }
2753 $stylesheet[] = $match[2][0];
2754 $offset = $match[0][1] + strlen($match[0][0]);
2755 }
2756 if ($offset < strlen($css)) {
2757 $stylesheet[] = substr($css, $offset);
2758 }
2759 return $stylesheet;
2760 }
2761 /**
2762 * Parse the selector part of a CSS rule into an array of selectors.
2763 *
2764 * Each selector will be an array with at offset 0, the string contents of
2765 * the selector. The rest of the array will be the class names (if any) that
2766 * must be present in the document for this selector to match.
2767 *
2768 * Null is returned if none of the selectors use classes, and can therefore
2769 * not be optimized.
2770 *
2771 * @param string $selectors
2772 * @return array|void
2773 */
2774 private function parseSelectors($selectors)
2775 {
2776 $newSelectors = [];
2777 $anyClasses = false;
2778 foreach (explode(',', $selectors) as $selector) {
2779 $classes = [$selector];
2780 if (preg_match_all("~\\.({$this->classNamePattern})~", $selector, $matches)) {
2781 foreach ($matches[1] as $class) {
2782 $classes[] = $class;
2783 $anyClasses = true;
2784 }
2785 }
2786 $newSelectors[] = $classes;
2787 }
2788 if (!$anyClasses) {
2789 return;
2790 }
2791 return $newSelectors;
2792 }
2793 private function getUsedClasses(\Traversable $elements)
2794 {
2795 $classes = [];
2796 /** @var Tag $tag */
2797 foreach ($elements as $tag) {
2798 if (!$tag instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag) {
2799 continue;
2800 }
2801 foreach (preg_split('/\\s+/', $tag->getAttribute('class')) as $cls) {
2802 if ($cls != '' && !isset($classes[$cls]) && preg_match("/^{$this->classNamePattern}\$/", $cls)) {
2803 $classes[$cls] = true;
2804 }
2805 }
2806 }
2807 return $classes;
2808 }
2809 private function removeEmptyMediaQueries($css)
2810 {
2811 return preg_replace('~@media\\s++[A-Z0-9():,\\s-]++\\s*+{}~i', '', $css);
2812 }
2813 }
2814 namespace Kibo\Phast\Filters\HTML\Minify;
2815
2816 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
2817 {
2818 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
2819 {
2820 $inTags = ['pre' => 0, 'textarea' => 0];
2821 foreach ($elements as $element) {
2822 if ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag && isset($inTags[$element->getTagName()])) {
2823 $inTags[$element->getTagName()]++;
2824 } elseif ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag && !empty($inTags[$element->getTagName()])) {
2825 $inTags[$element->getTagName()]--;
2826 } elseif ($element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Junk && !array_sum($inTags)) {
2827 $element->originalString = preg_replace_callback('~\\s++~', function ($match) {
2828 return strpos($match[0], "\n") === false ? ' ' : "\n";
2829 }, $element->originalString);
2830 }
2831 (yield $element);
2832 }
2833 }
2834 }
2835 namespace Kibo\Phast\Filters\HTML\DelayedIFrameLoading;
2836
2837 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
2838 {
2839 use \Kibo\Phast\Logging\LoggingTrait;
2840 protected $addScript = false;
2841 private $ignoredUrlPattern = '~
2842 ^about: |
2843 ^data:
2844 ~ix';
2845 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
2846 {
2847 return $tag->getTagName() == 'iframe' && $tag->hasAttribute('src');
2848 }
2849 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $iframe)
2850 {
2851 $src = trim($iframe->getAttribute('src'));
2852 if (preg_match($this->ignoredUrlPattern, $src)) {
2853 (yield $iframe);
2854 return;
2855 }
2856 $this->logger()->info('Delaying iframe {src}', ['src' => $src]);
2857 $iframe->setAttribute('data-phast-src', $src);
2858 $iframe->setAttribute('src', 'about:blank');
2859 $this->addScript = true;
2860 (yield $iframe);
2861 }
2862 protected function afterLoop()
2863 {
2864 if ($this->addScript) {
2865 $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"));
2866 }
2867 }
2868 }
2869 namespace Kibo\Phast\Filters\HTML\Diagnostics;
2870
2871 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
2872 {
2873 private $serviceUrl;
2874 public function __construct($serviceUrl)
2875 {
2876 $this->serviceUrl = $serviceUrl;
2877 }
2878 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
2879 {
2880 $url = (new \Kibo\Phast\Services\ServiceRequest())->withUrl(\Kibo\Phast\ValueObjects\URL::fromString($this->serviceUrl))->serialize();
2881 $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");
2882 $script->setConfig('diagnostics', ['serviceUrl' => $url]);
2883 $context->addPhastJavaScript($script);
2884 foreach ($elements as $element) {
2885 (yield $element);
2886 }
2887 }
2888 }
2889 namespace Kibo\Phast\Filters\HTML;
2890
2891 interface HTMLFilterFactory
2892 {
2893 /**
2894 * @param array $config
2895 * @return HTMLStreamFilter
2896 */
2897 public function make(array $config);
2898 }
2899 namespace Kibo\Phast\Filters\Image;
2900
2901 interface ImageFilter
2902 {
2903 /**
2904 * @param array $request
2905 * @return string
2906 */
2907 public function getCacheSalt(array $request);
2908 /**
2909 * @param Image $image
2910 * @param array $request
2911 * @return Image
2912 */
2913 public function transformImage(\Kibo\Phast\Filters\Image\Image $image, array $request);
2914 }
2915 namespace Kibo\Phast\Filters\Image;
2916
2917 interface ImageFilterFactory
2918 {
2919 /**
2920 * @param array $config
2921 * @return ImageFilter
2922 */
2923 public function make(array $config);
2924 }
2925 namespace Kibo\Phast\Filters\Image\Composite;
2926
2927 class Factory
2928 {
2929 /**
2930 * @var array
2931 */
2932 private $config;
2933 /**
2934 * CompositeImageFilterFactory constructor.
2935 *
2936 * @param array $config
2937 */
2938 public function __construct(array $config)
2939 {
2940 $this->config = $config;
2941 }
2942 public function make()
2943 {
2944 $imageFactoryClass = $this->config['images']['factory'];
2945 if (!class_exists($imageFactoryClass)) {
2946 throw new \Kibo\Phast\Exceptions\LogicException("No such class: {$imageFactoryClass}");
2947 }
2948 $composite = new \Kibo\Phast\Filters\Image\Composite\Filter(new $imageFactoryClass($this->config), (new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManagerFactory())->make($this->config));
2949 foreach ($this->config['images']['filters'] as $class => $config) {
2950 if ($config === null) {
2951 continue;
2952 }
2953 $package = \Kibo\Phast\Environment\Package::fromPackageClass($class);
2954 $filter = $package->getFactory()->make($this->config);
2955 $composite->addImageFilter($filter);
2956 }
2957 if ($this->config['images']['enable-cache']) {
2958 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']));
2959 }
2960 return $composite;
2961 }
2962 }
2963 namespace Kibo\Phast\Filters\Image;
2964
2965 class ImageFactory
2966 {
2967 private $config;
2968 public function __construct(array $config)
2969 {
2970 $this->config = $config;
2971 }
2972 /**
2973 * @param URL $url
2974 * @return Image
2975 */
2976 public function getForURL(\Kibo\Phast\ValueObjects\URL $url)
2977 {
2978 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
2979 $retriever->addRetriever(new \Kibo\Phast\Retrievers\LocalRetriever($this->config['retrieverMap']));
2980 $retriever->addRetriever((new \Kibo\Phast\Retrievers\RemoteRetrieverFactory())->make($this->config));
2981 return new \Kibo\Phast\Filters\Image\ImageImplementations\DefaultImage($url, $retriever);
2982 }
2983 /**
2984 * @param Resource $resource
2985 * @return Image
2986 */
2987 public function getForResource(\Kibo\Phast\ValueObjects\Resource $resource)
2988 {
2989 return $this->getForURL($resource->getUrl());
2990 }
2991 }
2992 namespace Kibo\Phast\Filters\Image\CommonDiagnostics;
2993
2994 class DiagnosticsRetriever implements \Kibo\Phast\Retrievers\Retriever
2995 {
2996 /**
2997 * @var string
2998 */
2999 private $file;
3000 /**
3001 * DiagnosticsRetriever constructor.
3002 * @param string $file
3003 */
3004 public function __construct($file)
3005 {
3006 $this->file = $file;
3007 }
3008 public function retrieve(\Kibo\Phast\ValueObjects\URL $url)
3009 {
3010 return file_get_contents($this->file);
3011 }
3012 public function getCacheSalt(\Kibo\Phast\ValueObjects\URL $url)
3013 {
3014 return '';
3015 }
3016 }
3017 namespace Kibo\Phast\Filters\Image;
3018
3019 interface Image
3020 {
3021 const TYPE_JPEG = 'image/jpeg';
3022 const TYPE_PNG = 'image/png';
3023 const TYPE_WEBP = 'image/webp';
3024 /**
3025 * @return integer
3026 */
3027 public function getWidth();
3028 /**
3029 * @return integer
3030 */
3031 public function getHeight();
3032 /**
3033 * @return string
3034 */
3035 public function getType();
3036 /**
3037 * @return string
3038 */
3039 public function getAsString();
3040 /**
3041 * @return integer
3042 */
3043 public function getSizeAsString();
3044 /**
3045 * @param integer $width
3046 * @param integer $height
3047 * @return Image
3048 */
3049 public function resize($width, $height);
3050 /**
3051 * @param integer $compression
3052 * @return Image
3053 */
3054 public function compress($compression);
3055 /**
3056 * @param string $type - One of Image::TYPE_JPEG, Image::TYPE_PNG or Image::TYPE_WEBP
3057 * @return Image
3058 */
3059 public function encodeTo($type);
3060 }
3061 namespace Kibo\Phast\Filters\Image\ImageAPIClient;
3062
3063 class Diagnostics implements \Kibo\Phast\Diagnostics\Diagnostics
3064 {
3065 public function diagnose(array $config)
3066 {
3067 $package = \Kibo\Phast\Environment\Package::fromPackageClass(get_class($this));
3068 /** @var ImageFilter $filter */
3069 $filter = $package->getFactory()->make($config);
3070 $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";
3071 if ($imageData === false) {
3072 throw new \Kibo\Phast\Exceptions\RuntimeException('Could not read testing image for ' . static::class . ' diagnostics.');
3073 }
3074 $image = new \Kibo\Phast\Filters\Image\ImageImplementations\DummyImage();
3075 $image->setImageString($imageData);
3076 $filter->transformImage($image, []);
3077 }
3078 }
3079 namespace Kibo\Phast\Filters\Image\ImageAPIClient;
3080
3081 class Factory implements \Kibo\Phast\Filters\Image\ImageFilterFactory
3082 {
3083 public function make(array $config)
3084 {
3085 $signature = new \Kibo\Phast\Security\ServiceSignature(new \Kibo\Phast\Cache\File\Cache($config['cache'], 'api-service-signature'));
3086 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));
3087 }
3088 }
3089 namespace Kibo\Phast\Filters\Image\ImageAPIClient;
3090
3091 class Filter implements \Kibo\Phast\Filters\Image\ImageFilter
3092 {
3093 /**
3094 * @var array
3095 */
3096 private $config;
3097 /**
3098 * @var ServiceSignature
3099 */
3100 private $signature;
3101 /**
3102 * @var Client
3103 */
3104 private $client;
3105 /**
3106 * Filter constructor.
3107 * @param array $config
3108 * @param ServiceSignature $signature
3109 * @param Client $client
3110 */
3111 public function __construct(array $config, \Kibo\Phast\Security\ServiceSignature $signature, \Kibo\Phast\HTTP\Client $client)
3112 {
3113 $this->config = $config;
3114 $this->signature = $signature;
3115 $this->client = $client;
3116 $this->signature->setIdentities('');
3117 }
3118 public function getCacheSalt(array $request)
3119 {
3120 $result = 'api-call';
3121 foreach (['width', 'height', 'preferredType'] as $key) {
3122 if (isset($request[$key])) {
3123 $result .= "-{$key}-{$request[$key]}";
3124 }
3125 }
3126 return $result;
3127 }
3128 public function transformImage(\Kibo\Phast\Filters\Image\Image $image, array $request)
3129 {
3130 $url = $this->getRequestURL($request);
3131 $headers = $this->getRequestHeaders($image, $request);
3132 $data = $image->getAsString();
3133 try {
3134 $response = $this->client->post(\Kibo\Phast\ValueObjects\URL::fromString($url), $data, $headers);
3135 } catch (\Exception $e) {
3136 throw new \Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException('Request exception: ' . get_class($e) . ' MSG: ' . $e->getMessage() . ' Code: ' . $e->getCode());
3137 }
3138 if (strlen($response->getContent()) === 0) {
3139 throw new \Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException('Image API response is empty');
3140 }
3141 $newImage = new \Kibo\Phast\Filters\Image\ImageImplementations\DummyImage();
3142 $newImage->setImageString($response->getContent());
3143 $headers = [];
3144 foreach ($response->getHeaders() as $name => $value) {
3145 $headers[strtolower($name)] = $value;
3146 }
3147 $newImage->setType($headers['content-type']);
3148 return $newImage;
3149 }
3150 private function getRequestURL(array $request)
3151 {
3152 $params = [];
3153 foreach (['width', 'height'] as $key) {
3154 if (isset($request[$key])) {
3155 $params[$key] = $request[$key];
3156 }
3157 }
3158 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);
3159 }
3160 private function getRequestHeaders(\Kibo\Phast\Filters\Image\Image $image, array $request)
3161 {
3162 $headers = ['X-Phast-Image-API-Client' => $this->getRequestToken(), 'Content-Type' => 'application/octet-stream'];
3163 if (isset($request['preferredType']) && $request['preferredType'] == \Kibo\Phast\Filters\Image\Image::TYPE_WEBP) {
3164 $headers['Accept'] = 'image/webp';
3165 }
3166 return $headers;
3167 }
3168 private function getRequestToken()
3169 {
3170 $token_parts = [];
3171 foreach (['host-name', 'request-uri', 'plugin-version'] as $key) {
3172 $token_parts[$key] = $this->config[$key];
3173 }
3174 $token_parts['php'] = PHP_VERSION;
3175 return json_encode($token_parts);
3176 }
3177 }
3178 namespace Kibo\Phast\Filters\Image\ImageImplementations;
3179
3180 abstract class BaseImage
3181 {
3182 /**
3183 * @var integer
3184 */
3185 protected $width;
3186 /**
3187 * @var integer
3188 */
3189 protected $height;
3190 /**
3191 * @var integer
3192 */
3193 protected $compression;
3194 /**
3195 * @var string
3196 */
3197 protected $type;
3198 /**
3199 * @return string
3200 */
3201 public abstract function getAsString();
3202 /**
3203 * @return integer
3204 */
3205 public function getSizeAsString()
3206 {
3207 return strlen($this->getAsString());
3208 }
3209 /**
3210 * @param $width
3211 * @param $height
3212 * @return static
3213 */
3214 public function resize($width, $height)
3215 {
3216 $im = clone $this;
3217 $im->width = $width;
3218 $im->height = $height;
3219 return $im;
3220 }
3221 /**
3222 * @param $compression
3223 * @return static
3224 */
3225 public function compress($compression)
3226 {
3227 $im = clone $this;
3228 $im->compression = $compression;
3229 return $im;
3230 }
3231 /**
3232 * @param $type
3233 * @return static
3234 */
3235 public function encodeTo($type)
3236 {
3237 $im = clone $this;
3238 $im->type = $type;
3239 return $im;
3240 }
3241 }
3242 namespace Kibo\Phast\Filters\Image\ImageImplementations;
3243
3244 class DefaultImage extends \Kibo\Phast\Filters\Image\ImageImplementations\BaseImage implements \Kibo\Phast\Filters\Image\Image
3245 {
3246 /**
3247 * @var URL
3248 */
3249 private $imageURL;
3250 /**
3251 * @var Retriever
3252 */
3253 private $retriever;
3254 /**
3255 * @var string
3256 */
3257 private $imageString;
3258 /**
3259 * @var array
3260 */
3261 private $imageInfo;
3262 /**
3263 * @var ObjectifiedFunctions
3264 */
3265 private $funcs;
3266 public function __construct(\Kibo\Phast\ValueObjects\URL $imageURL, \Kibo\Phast\Retrievers\Retriever $retriever, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
3267 {
3268 $this->imageURL = $imageURL;
3269 $this->retriever = $retriever;
3270 $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs;
3271 }
3272 public function getWidth()
3273 {
3274 return isset($this->width) ? $this->width : $this->getImageInfo()[0];
3275 }
3276 public function getHeight()
3277 {
3278 return isset($this->height) ? $this->height : $this->getImageInfo()[1];
3279 }
3280 public function getType()
3281 {
3282 if (isset($this->type)) {
3283 return $this->type;
3284 }
3285 $type = @image_type_to_mime_type($this->getImageInfo()[2]);
3286 if (!$type) {
3287 throw new \Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException('Could not determine image type');
3288 }
3289 return $type;
3290 }
3291 public function getAsString()
3292 {
3293 return $this->getImageString();
3294 }
3295 private function getImageString()
3296 {
3297 if (!isset($this->imageString)) {
3298 $imageString = $this->retriever->retrieve($this->imageURL);
3299 if ($imageString === false) {
3300 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('Could not find image: ' . $this->imageURL, 0, null, $this->imageURL);
3301 }
3302 $this->imageString = $imageString;
3303 }
3304 return $this->imageString;
3305 }
3306 private function getImageInfo()
3307 {
3308 if (!isset($this->imageInfo)) {
3309 if ($this->getImageString() === '') {
3310 throw new \Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException('Image is empty');
3311 }
3312 $imageInfo = @getimagesizefromstring($this->getImageString());
3313 if ($imageInfo === false) {
3314 throw new \Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException('Could not read GD image info');
3315 }
3316 $this->imageInfo = $imageInfo;
3317 }
3318 return $this->imageInfo;
3319 }
3320 protected function __clone()
3321 {
3322 throw new \Kibo\Phast\Exceptions\LogicException('No operations may be performed on DefaultImage');
3323 }
3324 }
3325 namespace Kibo\Phast\Filters\CSS\CSSMinifier;
3326
3327 class Factory
3328 {
3329 public function make()
3330 {
3331 return new \Kibo\Phast\Filters\CSS\CSSMinifier\Filter();
3332 }
3333 }
3334 namespace Kibo\Phast\Filters\CSS\CSSURLRewriter;
3335
3336 class Factory
3337 {
3338 public function make()
3339 {
3340 return new \Kibo\Phast\Filters\CSS\CSSURLRewriter\Filter();
3341 }
3342 }
3343 namespace Kibo\Phast\Filters\CSS\ImageURLRewriter;
3344
3345 class Factory
3346 {
3347 public function make(array $config)
3348 {
3349 return new \Kibo\Phast\Filters\CSS\ImageURLRewriter\Filter((new \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriterFactory())->make($config, \Kibo\Phast\Filters\CSS\ImageURLRewriter\Filter::class));
3350 }
3351 }
3352 namespace Kibo\Phast\Filters\CSS\Composite;
3353
3354 class Factory
3355 {
3356 /**
3357 * @param array $config
3358 * @return Filter
3359 */
3360 public function make(array $config)
3361 {
3362 $class = \Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS\Filter::class;
3363 if (isset($config['documents']['filters'][$class]['serviceUrl'])) {
3364 $serviceUrl = $config['documents']['filters'][$class]['serviceUrl'];
3365 } else {
3366 $serviceUrl = $config['servicesUrl'] . '?service=images';
3367 }
3368 $filter = new \Kibo\Phast\Filters\CSS\Composite\Filter($serviceUrl);
3369 foreach (array_keys($config['styles']['filters']) as $filterClass) {
3370 $filter->addFilter(\Kibo\Phast\Environment\Package::fromPackageClass($filterClass)->getFactory()->make($config));
3371 }
3372 return $filter;
3373 }
3374 }
3375 namespace Kibo\Phast\Filters\CSS\FontSwap;
3376
3377 class Factory
3378 {
3379 public function make()
3380 {
3381 return new \Kibo\Phast\Filters\CSS\FontSwap\Filter();
3382 }
3383 }
3384 namespace Kibo\Phast\Filters\CSS\ImportsStripper;
3385
3386 class Factory
3387 {
3388 public function make()
3389 {
3390 return new \Kibo\Phast\Filters\CSS\ImportsStripper\Filter();
3391 }
3392 }
3393 namespace Kibo\Phast\Filters\Text\Decode;
3394
3395 class Factory
3396 {
3397 public function make()
3398 {
3399 return new \Kibo\Phast\Filters\Text\Decode\Filter();
3400 }
3401 }
3402 namespace Kibo\Phast\JSMin;
3403
3404 class UnterminatedRegExpException extends \Exception
3405 {
3406 }
3407 namespace Kibo\Phast\JSMin;
3408
3409 class UnterminatedCommentException extends \Exception
3410 {
3411 }
3412 namespace Kibo\Phast\JSMin;
3413
3414 class UnterminatedStringException extends \Exception
3415 {
3416 }
3417 namespace Kibo\Phast\JSMin;
3418
3419 /**
3420 * JSMin.php - modified PHP implementation of Douglas Crockford's JSMin.
3421 *
3422 * <code>
3423 * $minifiedJs = JSMin::minify($js);
3424 * </code>
3425 *
3426 * This is a modified port of jsmin.c. Improvements:
3427 *
3428 * Does not choke on some regexp literals containing quote characters. E.g. /'/
3429 *
3430 * Spaces are preserved after some add/sub operators, so they are not mistakenly
3431 * converted to post-inc/dec. E.g. a + ++b -> a+ ++b
3432 *
3433 * Preserves multi-line comments that begin with /*!
3434 *
3435 * PHP 5 or higher is required.
3436 *
3437 * Permission is hereby granted to use this version of the library under the
3438 * same terms as jsmin.c, which has the following license:
3439 *
3440 * --
3441 * Copyright (c) 2002 Douglas Crockford (www.crockford.com)
3442 *
3443 * Permission is hereby granted, free of charge, to any person obtaining a copy of
3444 * this software and associated documentation files (the "Software"), to deal in
3445 * the Software without restriction, including without limitation the rights to
3446 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
3447 * of the Software, and to permit persons to whom the Software is furnished to do
3448 * so, subject to the following conditions:
3449 *
3450 * The above copyright notice and this permission notice shall be included in all
3451 * copies or substantial portions of the Software.
3452 *
3453 * The Software shall be used for Good, not Evil.
3454 *
3455 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3456 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
3457 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
3458 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
3459 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3460 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3461 * SOFTWARE.
3462 * --
3463 *
3464 * @package JSMin
3465 * @author Ryan Grove <ryan@wonko.com> (PHP port)
3466 * @author Steve Clay <steve@mrclay.org> (modifications + cleanup)
3467 * @author Andrea Giammarchi <http://www.3site.eu> (spaceBeforeRegExp)
3468 * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
3469 * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
3470 * @license http://opensource.org/licenses/mit-license.php MIT License
3471 * @link http://code.google.com/p/jsmin-php/
3472 */
3473 class JSMin
3474 {
3475 const ACTION_KEEP_A = 1;
3476 const ACTION_DELETE_A = 2;
3477 const ACTION_DELETE_A_B = 3;
3478 protected $a = "\n";
3479 protected $b = '';
3480 protected $input = '';
3481 protected $inputIndex = 0;
3482 protected $inputLength = 0;
3483 protected $lookAhead = null;
3484 protected $output = '';
3485 protected $lastByteOut = '';
3486 protected $keptComment = '';
3487 /**
3488 * Minify Javascript.
3489 *
3490 * @param string $js Javascript to be minified
3491 *
3492 * @return string
3493 */
3494 public static function minify($js)
3495 {
3496 $jsmin = new \Kibo\Phast\JSMin\JSMin($js);
3497 return $jsmin->min();
3498 }
3499 /**
3500 * @param string $input
3501 */
3502 public function __construct($input)
3503 {
3504 $this->input = $input;
3505 }
3506 /**
3507 * Perform minification, return result
3508 *
3509 * @return string
3510 */
3511 public function min()
3512 {
3513 if ($this->output !== '') {
3514 // min already run
3515 return $this->output;
3516 }
3517 $mbIntEnc = null;
3518 if (function_exists('mb_strlen') && (int) ini_get('mbstring.func_overload') & 2) {
3519 $mbIntEnc = mb_internal_encoding();
3520 mb_internal_encoding('8bit');
3521 }
3522 if (isset($this->input[0]) && $this->input[0] === "\357") {
3523 $this->input = substr($this->input, 3);
3524 }
3525 $this->input = str_replace("\r\n", "\n", $this->input);
3526 $this->inputLength = strlen($this->input);
3527 $this->action(self::ACTION_DELETE_A_B);
3528 while ($this->a !== null) {
3529 // determine next command
3530 $command = self::ACTION_KEEP_A;
3531 // default
3532 if ($this->isWhiteSpace($this->a)) {
3533 if (($this->lastByteOut === '+' || $this->lastByteOut === '-') && $this->b === $this->lastByteOut) {
3534 // Don't delete this space. If we do, the addition/subtraction
3535 // could be parsed as a post-increment
3536 } elseif (!$this->isAlphaNum($this->b)) {
3537 $command = self::ACTION_DELETE_A;
3538 }
3539 } elseif ($this->isLineTerminator($this->a)) {
3540 if ($this->isWhiteSpace($this->b)) {
3541 $command = self::ACTION_DELETE_A_B;
3542 // in case of mbstring.func_overload & 2, must check for null b,
3543 // otherwise mb_strpos will give WARNING
3544 } elseif ($this->b === null || false === strpos('{[(+-!~', $this->b) && !$this->isAlphaNum($this->b)) {
3545 $command = self::ACTION_DELETE_A;
3546 }
3547 } elseif (!$this->isAlphaNum($this->a)) {
3548 if ($this->isWhiteSpace($this->b) || $this->isLineTerminator($this->b) && false === strpos('}])+-"\'', $this->a)) {
3549 $command = self::ACTION_DELETE_A_B;
3550 }
3551 }
3552 $this->action($command);
3553 }
3554 $this->output = trim($this->output);
3555 if ($mbIntEnc !== null) {
3556 mb_internal_encoding($mbIntEnc);
3557 }
3558 return $this->output;
3559 }
3560 /**
3561 * ACTION_KEEP_A = Output A. Copy B to A. Get the next B.
3562 * ACTION_DELETE_A = Copy B to A. Get the next B.
3563 * ACTION_DELETE_A_B = Get the next B.
3564 *
3565 * @param int $command
3566 * @throws UnterminatedRegExpException|UnterminatedStringException
3567 */
3568 protected function action($command)
3569 {
3570 // make sure we don't compress "a + ++b" to "a+++b", etc.
3571 if ($command === self::ACTION_DELETE_A_B && $this->b === ' ' && ($this->a === '+' || $this->a === '-')) {
3572 // Note: we're at an addition/substraction operator; the inputIndex
3573 // will certainly be a valid index
3574 if ($this->input[$this->inputIndex] === $this->a) {
3575 // This is "+ +" or "- -". Don't delete the space.
3576 $command = self::ACTION_KEEP_A;
3577 }
3578 }
3579 switch ($command) {
3580 case self::ACTION_KEEP_A:
3581 // 1
3582 $this->output .= $this->a;
3583 if ($this->keptComment) {
3584 $this->output = rtrim($this->output, "\n");
3585 $this->output .= $this->keptComment;
3586 $this->keptComment = '';
3587 }
3588 $this->lastByteOut = $this->a;
3589 // fallthrough intentional
3590 case self::ACTION_DELETE_A:
3591 // 2
3592 $this->a = $this->b;
3593 if ($this->a === "'" || $this->a === '"' || $this->a === '`') {
3594 // string/template literal
3595 $delimiter = $this->a;
3596 $str = $this->a;
3597 // in case needed for exception
3598 for (;;) {
3599 $this->output .= $this->a;
3600 $this->lastByteOut = $this->a;
3601 $this->a = $this->get();
3602 if ($this->a === $this->b) {
3603 // end quote
3604 break;
3605 }
3606 if ($delimiter === '`' && $this->isLineTerminator($this->a)) {
3607 // leave the newline
3608 } elseif ($this->isEOF($this->a)) {
3609 $byte = $this->inputIndex - 1;
3610 throw new \Kibo\Phast\JSMin\UnterminatedStringException("JSMin: Unterminated String at byte {$byte}: {$str}");
3611 }
3612 $str .= $this->a;
3613 if ($this->a === '\\') {
3614 $this->output .= $this->a;
3615 $this->lastByteOut = $this->a;
3616 $this->a = $this->get();
3617 $str .= $this->a;
3618 }
3619 }
3620 }
3621 // fallthrough intentional
3622 case self::ACTION_DELETE_A_B:
3623 // 3
3624 $this->b = $this->next();
3625 if ($this->b === '/' && $this->isRegexpLiteral()) {
3626 $this->output .= $this->a . $this->b;
3627 $pattern = '/';
3628 // keep entire pattern in case we need to report it in the exception
3629 for (;;) {
3630 $this->a = $this->get();
3631 $pattern .= $this->a;
3632 if ($this->a === '[') {
3633 for (;;) {
3634 $this->output .= $this->a;
3635 $this->a = $this->get();
3636 $pattern .= $this->a;
3637 if ($this->a === ']') {
3638 break;
3639 }
3640 if ($this->a === '\\') {
3641 $this->output .= $this->a;
3642 $this->a = $this->get();
3643 $pattern .= $this->a;
3644 }
3645 if ($this->isEOF($this->a)) {
3646 throw new \Kibo\Phast\JSMin\UnterminatedRegExpException("JSMin: Unterminated set in RegExp at byte " . $this->inputIndex . ": {$pattern}");
3647 }
3648 }
3649 }
3650 if ($this->a === '/') {
3651 // end pattern
3652 break;
3653 // while (true)
3654 } elseif ($this->a === '\\') {
3655 $this->output .= $this->a;
3656 $this->a = $this->get();
3657 $pattern .= $this->a;
3658 } elseif ($this->isEOF($this->a)) {
3659 $byte = $this->inputIndex - 1;
3660 throw new \Kibo\Phast\JSMin\UnterminatedRegExpException("JSMin: Unterminated RegExp at byte {$byte}: {$pattern}");
3661 }
3662 $this->output .= $this->a;
3663 $this->lastByteOut = $this->a;
3664 }
3665 $this->b = $this->next();
3666 }
3667 }
3668 }
3669 /**
3670 * @return bool
3671 */
3672 protected function isRegexpLiteral()
3673 {
3674 if (false !== strpos("(,=:[!&|?+-~*{;", $this->a)) {
3675 // we can't divide after these tokens
3676 return true;
3677 }
3678 // check if first non-ws token is "/" (see starts-regex.js)
3679 $length = strlen($this->output);
3680 if ($this->isWhiteSpace($this->a) || $this->isLineTerminator($this->a)) {
3681 if ($length < 2) {
3682 // weird edge case
3683 return true;
3684 }
3685 }
3686 // if the "/" follows a keyword, it must be a regexp, otherwise it's best to assume division
3687 $subject = $this->output . trim($this->a);
3688 if (!preg_match('/(?:case|else|in|return|typeof)$/', $subject, $m)) {
3689 // not a keyword
3690 return false;
3691 }
3692 // can't be sure it's a keyword yet (see not-regexp.js)
3693 $charBeforeKeyword = substr($subject, 0 - strlen($m[0]) - 1, 1);
3694 if ($this->isAlphaNum($charBeforeKeyword)) {
3695 // this is really an identifier ending in a keyword, e.g. "xreturn"
3696 return false;
3697 }
3698 // it's a regexp. Remove unneeded whitespace after keyword
3699 if ($this->isWhiteSpace($this->a) || $this->isLineTerminator($this->a)) {
3700 $this->a = '';
3701 }
3702 return true;
3703 }
3704 /**
3705 * Return the next character from stdin. Watch out for lookahead. If the character is a control character,
3706 * translate it to a space or linefeed.
3707 *
3708 * @return string
3709 */
3710 protected function get()
3711 {
3712 $c = $this->lookAhead;
3713 $this->lookAhead = null;
3714 if ($c === null) {
3715 // getc(stdin)
3716 if ($this->inputIndex < $this->inputLength) {
3717 $c = $this->input[$this->inputIndex];
3718 $this->inputIndex += 1;
3719 } else {
3720 $c = null;
3721 }
3722 }
3723 if ($c === "\r") {
3724 return "\n";
3725 }
3726 return $c;
3727 }
3728 /**
3729 * Does $a indicate end of input?
3730 *
3731 * @param string $a
3732 * @return bool
3733 */
3734 protected function isEOF($a)
3735 {
3736 return $a === null || $this->isLineTerminator($a);
3737 }
3738 /**
3739 * Get next char (without getting it). If is ctrl character, translate to a space or newline.
3740 *
3741 * @return string
3742 */
3743 protected function peek()
3744 {
3745 $this->lookAhead = $this->get();
3746 return $this->lookAhead;
3747 }
3748 /**
3749 * Return true if the character is a letter, digit, underscore, dollar sign, or non-ASCII character.
3750 *
3751 * @param string $c
3752 *
3753 * @return bool
3754 */
3755 protected function isAlphaNum($c)
3756 {
3757 return preg_match('/^[a-z0-9A-Z_\\$\\\\]$/', $c) || ord($c) > 126;
3758 }
3759 /**
3760 * Consume a single line comment from input (possibly retaining it)
3761 */
3762 protected function consumeSingleLineComment()
3763 {
3764 $comment = '';
3765 while (true) {
3766 $get = $this->get();
3767 $comment .= $get;
3768 if ($this->isEOF($get)) {
3769 // if IE conditional comment
3770 if (preg_match('/^\\/@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
3771 $this->keptComment .= "/{$comment}";
3772 }
3773 return;
3774 }
3775 }
3776 }
3777 /**
3778 * Consume a multiple line comment from input (possibly retaining it)
3779 *
3780 * @throws UnterminatedCommentException
3781 */
3782 protected function consumeMultipleLineComment()
3783 {
3784 $this->get();
3785 $comment = '';
3786 for (;;) {
3787 $get = $this->get();
3788 if ($get === '*') {
3789 if ($this->peek() === '/') {
3790 // end of comment reached
3791 $this->get();
3792 if (0 === strpos($comment, '!')) {
3793 // preserved by YUI Compressor
3794 if (!$this->keptComment) {
3795 // don't prepend a newline if two comments right after one another
3796 $this->keptComment = "\n";
3797 }
3798 $this->keptComment .= "/*!" . substr($comment, 1) . "*/\n";
3799 } else {
3800 if (preg_match('/^@(?:cc_on|if|elif|else|end)\\b/', $comment)) {
3801 // IE conditional
3802 $this->keptComment .= "/*{$comment}*/";
3803 }
3804 }
3805 return;
3806 }
3807 } elseif ($get === null) {
3808 throw new \Kibo\Phast\JSMin\UnterminatedCommentException("JSMin: Unterminated comment at byte {$this->inputIndex}: /*{$comment}");
3809 }
3810 $comment .= $get;
3811 }
3812 }
3813 /**
3814 * Get the next character, skipping over comments. Some comments may be preserved.
3815 *
3816 * @return string
3817 */
3818 protected function next()
3819 {
3820 $get = $this->get();
3821 if ($get === '/') {
3822 switch ($this->peek()) {
3823 case '/':
3824 $this->consumeSingleLineComment();
3825 $get = "\n";
3826 break;
3827 case '*':
3828 $this->consumeMultipleLineComment();
3829 $get = ' ';
3830 break;
3831 }
3832 }
3833 return $get;
3834 }
3835 protected function isWhiteSpace($s)
3836 {
3837 // https://www.ecma-international.org/ecma-262/#sec-white-space
3838 return $s !== null && strpos(" \t\v\f", $s) !== false;
3839 }
3840 protected function isLineTerminator($s)
3841 {
3842 // https://www.ecma-international.org/ecma-262/#sec-line-terminators
3843 return $s !== null && strpos("\n\r", $s) !== false;
3844 }
3845 }
3846 namespace Kibo\Phast\Parsing\HTML;
3847
3848 class PCRETokenizer
3849 {
3850 private $mainPattern = '~
3851 # Allow duplicate names for subpatterns
3852 (?J)
3853
3854 (
3855 @@COMMENT |
3856 @@SCRIPT |
3857 @@STYLE |
3858 @@CLOSING_TAG |
3859 @@TAG
3860 )
3861 ~Xxsi';
3862 private $attributePattern = '~
3863 @attr
3864 ~Xxsi';
3865 private $subroutines = array('COMMENT' => '
3866 <!--.*?-->
3867 ', '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' => '
3868 (?: @attr )*+
3869 ', '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' => '
3870 </ @@tag_name [^>]*+ >
3871 ');
3872 public function __construct()
3873 {
3874 $this->mainPattern = $this->compilePattern($this->mainPattern, $this->subroutines);
3875 $this->attributePattern = $this->compilePattern($this->attributePattern, $this->subroutines);
3876 }
3877 public function tokenize($data)
3878 {
3879 $offset = 0;
3880 while (preg_match($this->mainPattern, $data, $match, PREG_OFFSET_CAPTURE, $offset)) {
3881 if ($match[0][1] > $offset) {
3882 $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Junk();
3883 $element->originalString = substr($data, $offset, $match[0][1] - $offset);
3884 (yield $element);
3885 }
3886 if (!empty($match['COMMENT'][0])) {
3887 $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Comment();
3888 $element->originalString = $match[0][0];
3889 } elseif (!empty($match['TAG'][0]) || !empty($match['SCRIPT'][0]) || !empty($match['STYLE'][0])) {
3890 $attributes = $match['attrs'][0] === '' ? [] : $this->parseAttributes($match['attrs'][0]);
3891 $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag($match['tag_name'][0], $attributes);
3892 $element->originalString = $match['TAG'][0];
3893 if (isset($match['body'][1]) && $match['body'][1] != -1) {
3894 $element->setTextContent($match['body'][0]);
3895 $element = $element->withClosingTag($match['closing_tag'][0]);
3896 }
3897 } elseif (!empty($match['CLOSING_TAG'][0])) {
3898 $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\ClosingTag($match['tag_name'][0]);
3899 $element->originalString = $match[0][0];
3900 } else {
3901 throw new \Kibo\Phast\Exceptions\RuntimeException("Unhandled match:\n" . \Kibo\Phast\Common\JSON::prettyEncode($match));
3902 }
3903 (yield $element);
3904 $offset = $match[0][1] + strlen($match[0][0]);
3905 }
3906 if ($offset < strlen($data) - 1) {
3907 $element = new \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Junk();
3908 $element->originalString = substr($data, $offset);
3909 (yield $element);
3910 }
3911 }
3912 private function parseAttributes($str)
3913 {
3914 $matches = $this->repeatMatch($this->attributePattern, $str);
3915 foreach ($matches as $match) {
3916 (yield $match['attr_name'][0] => isset($match['attr_value'][0]) ? html_entity_decode($match['attr_value'][0], ENT_QUOTES, 'UTF-8') : '');
3917 }
3918 }
3919 private function repeatMatch($pattern, $subject)
3920 {
3921 $offset = 0;
3922 while (preg_match($pattern, $subject, $match, PREG_OFFSET_CAPTURE, $offset)) {
3923 (yield $match);
3924 $offset = $match[0][1] + strlen($match[0][0]);
3925 }
3926 if ($offset < strlen($subject) - 1) {
3927 throw new \Kibo\Phast\Exceptions\RuntimeException('Unmatched part of subject: ' . substr($subject, $offset));
3928 }
3929 }
3930 /**
3931 * Replace subroutines in patterns
3932 */
3933 private function compilePattern($pattern, array $subroutines)
3934 {
3935 return preg_replace_callback('/@(@?)(\\w+)/', function ($match) use($subroutines) {
3936 $capture = !empty($match[1]);
3937 $ref = $match[2];
3938 if (!isset($subroutines[$ref])) {
3939 throw new \Kibo\Phast\Exceptions\RuntimeException("Unknown pattern '{$ref}' used, or circular reference");
3940 }
3941 $subroutine = $subroutines[$ref];
3942 unset($subroutines[$ref]);
3943 $replace = $this->compilePattern($subroutine, $subroutines);
3944 if ($capture) {
3945 $replace = "(?'{$ref}'{$replace})";
3946 } else {
3947 $replace = "(?:{$replace})";
3948 }
3949 return $replace;
3950 }, $pattern);
3951 }
3952 }
3953 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
3954
3955 class Element
3956 {
3957 /**
3958 * @var string
3959 */
3960 public $originalString;
3961 /**
3962 * @param string $originalString
3963 */
3964 public function setOriginalString($originalString)
3965 {
3966 $this->originalString = $originalString;
3967 }
3968 public function __get($name)
3969 {
3970 $method = 'get' . ucfirst($name);
3971 if (method_exists($this, $method)) {
3972 return call_user_func([$this, $method]);
3973 }
3974 }
3975 public function __set($name, $value)
3976 {
3977 $method = 'set' . ucfirst($name);
3978 if (method_exists($this, $method)) {
3979 return call_user_func([$this, $method], $value);
3980 }
3981 }
3982 public function toString()
3983 {
3984 return $this->__toString();
3985 }
3986 public function __toString()
3987 {
3988 return isset($this->originalString) ? $this->originalString : '';
3989 }
3990 public function dump()
3991 {
3992 return '<' . preg_replace('~^.*\\\\~', '', get_class($this)) . ' ' . $this->dumpValue() . '>';
3993 }
3994 public function dumpValue()
3995 {
3996 return \Kibo\Phast\Common\JSON::encode($this->originalString);
3997 }
3998 }
3999 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
4000
4001 class Junk extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element
4002 {
4003 }
4004 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
4005
4006 class Comment extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element
4007 {
4008 public function isIEConditional()
4009 {
4010 return (bool) preg_match('/^<!--\\[if\\s/', $this->originalString);
4011 }
4012 }
4013 /**
4014 * Provide general element functions.
4015 */
4016 namespace Kibo\Phast\Parsing\HTML;
4017
4018 /**
4019 * This class provides general information about HTML5 elements,
4020 * including syntactic and semantic issues.
4021 * Parsers and serializers can
4022 * use this class as a reference point for information about the rules
4023 * of various HTML5 elements.
4024 *
4025 * @todo consider using a bitmask table lookup. There is enough overlap in
4026 * naming that this could significantly shrink the size and maybe make it
4027 * faster. See the Go teams implementation at https://code.google.com/p/go/source/browse/html/atom.
4028 */
4029 class HTMLInfo
4030 {
4031 /**
4032 * Indicates an element is described in the specification.
4033 */
4034 const KNOWN_ELEMENT = 1;
4035 // From section 8.1.2: "script", "style"
4036 // From 8.2.5.4.7 ("in body" insertion mode): "noembed"
4037 // From 8.4 "style", "xmp", "iframe", "noembed", "noframes"
4038 /**
4039 * Indicates the contained text should be processed as raw text.
4040 */
4041 const TEXT_RAW = 2;
4042 // From section 8.1.2: "textarea", "title"
4043 /**
4044 * Indicates the contained text should be processed as RCDATA.
4045 */
4046 const TEXT_RCDATA = 4;
4047 /**
4048 * Indicates the tag cannot have content.
4049 */
4050 const VOID_TAG = 8;
4051 // "address", "article", "aside", "blockquote", "center", "details", "dialog", "dir", "div", "dl",
4052 // "fieldset", "figcaption", "figure", "footer", "header", "hgroup", "menu",
4053 // "nav", "ol", "p", "section", "summary", "ul"
4054 // "h1", "h2", "h3", "h4", "h5", "h6"
4055 // "pre", "listing"
4056 // "form"
4057 // "plaintext"
4058 /**
4059 * Indicates that if a previous event is for a P tag, that element
4060 * should be considered closed.
4061 */
4062 const AUTOCLOSE_P = 16;
4063 /**
4064 * Indicates that the text inside is plaintext (pre).
4065 */
4066 const TEXT_PLAINTEXT = 32;
4067 // See https://developer.mozilla.org/en-US/docs/HTML/Block-level_elements
4068 /**
4069 * Indicates that the tag is a block.
4070 */
4071 const BLOCK_TAG = 64;
4072 /**
4073 * Indicates that the tag allows only inline elements as child nodes.
4074 */
4075 const BLOCK_ONLY_INLINE = 128;
4076 /**
4077 * The HTML5 elements as defined in http://dev.w3.org/html5/markup/elements.html.
4078 *
4079 * @var array
4080 */
4081 public static $html5 = array(
4082 'a' => 1,
4083 'abbr' => 1,
4084 'address' => 65,
4085 // NORMAL | BLOCK_TAG
4086 'area' => 9,
4087 // NORMAL | VOID_TAG
4088 'article' => 81,
4089 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4090 'aside' => 81,
4091 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4092 'audio' => 65,
4093 // NORMAL | BLOCK_TAG
4094 'b' => 1,
4095 'base' => 9,
4096 // NORMAL | VOID_TAG
4097 'bdi' => 1,
4098 'bdo' => 1,
4099 'blockquote' => 81,
4100 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4101 'body' => 1,
4102 'br' => 9,
4103 // NORMAL | VOID_TAG
4104 'button' => 1,
4105 'canvas' => 65,
4106 // NORMAL | BLOCK_TAG
4107 'caption' => 1,
4108 'cite' => 1,
4109 'code' => 1,
4110 'col' => 9,
4111 // NORMAL | VOID_TAG
4112 'colgroup' => 1,
4113 'command' => 9,
4114 // NORMAL | VOID_TAG
4115 // "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
4116 'datalist' => 1,
4117 'dd' => 65,
4118 // NORMAL | BLOCK_TAG
4119 'del' => 1,
4120 'details' => 17,
4121 // NORMAL | AUTOCLOSE_P,
4122 'dfn' => 1,
4123 'dialog' => 17,
4124 // NORMAL | AUTOCLOSE_P,
4125 'div' => 81,
4126 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4127 'dl' => 81,
4128 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4129 'dt' => 1,
4130 'em' => 1,
4131 'embed' => 9,
4132 // NORMAL | VOID_TAG
4133 'fieldset' => 81,
4134 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4135 'figcaption' => 81,
4136 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4137 'figure' => 81,
4138 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4139 'footer' => 81,
4140 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4141 'form' => 81,
4142 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4143 'h1' => 81,
4144 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4145 'h2' => 81,
4146 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4147 'h3' => 81,
4148 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4149 'h4' => 81,
4150 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4151 'h5' => 81,
4152 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4153 'h6' => 81,
4154 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4155 'head' => 1,
4156 'header' => 81,
4157 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4158 'hgroup' => 81,
4159 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4160 'hr' => 73,
4161 // NORMAL | VOID_TAG
4162 'html' => 1,
4163 'i' => 1,
4164 'iframe' => 3,
4165 // NORMAL | TEXT_RAW
4166 'img' => 9,
4167 // NORMAL | VOID_TAG
4168 'input' => 9,
4169 // NORMAL | VOID_TAG
4170 'kbd' => 1,
4171 'ins' => 1,
4172 'keygen' => 9,
4173 // NORMAL | VOID_TAG
4174 'label' => 1,
4175 'legend' => 1,
4176 'li' => 1,
4177 'link' => 9,
4178 // NORMAL | VOID_TAG
4179 'map' => 1,
4180 'mark' => 1,
4181 'menu' => 17,
4182 // NORMAL | AUTOCLOSE_P,
4183 'meta' => 9,
4184 // NORMAL | VOID_TAG
4185 'meter' => 1,
4186 'nav' => 17,
4187 // NORMAL | AUTOCLOSE_P,
4188 'noscript' => 65,
4189 // NORMAL | BLOCK_TAG
4190 'object' => 1,
4191 'ol' => 81,
4192 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4193 'optgroup' => 1,
4194 'option' => 1,
4195 'output' => 65,
4196 // NORMAL | BLOCK_TAG
4197 'p' => 209,
4198 // NORMAL | AUTOCLOSE_P | BLOCK_TAG | BLOCK_ONLY_INLINE
4199 'param' => 9,
4200 // NORMAL | VOID_TAG
4201 'pre' => 81,
4202 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4203 'progress' => 1,
4204 'q' => 1,
4205 'rp' => 1,
4206 'rt' => 1,
4207 'ruby' => 1,
4208 's' => 1,
4209 'samp' => 1,
4210 'script' => 3,
4211 // NORMAL | TEXT_RAW
4212 'section' => 81,
4213 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4214 'select' => 1,
4215 'small' => 1,
4216 'source' => 9,
4217 // NORMAL | VOID_TAG
4218 'span' => 1,
4219 'strong' => 1,
4220 'style' => 3,
4221 // NORMAL | TEXT_RAW
4222 'sub' => 1,
4223 'summary' => 17,
4224 // NORMAL | AUTOCLOSE_P,
4225 'sup' => 1,
4226 'table' => 65,
4227 // NORMAL | BLOCK_TAG
4228 'tbody' => 1,
4229 'td' => 1,
4230 'textarea' => 5,
4231 // NORMAL | TEXT_RCDATA
4232 'tfoot' => 65,
4233 // NORMAL | BLOCK_TAG
4234 'th' => 1,
4235 'thead' => 1,
4236 'time' => 1,
4237 'title' => 5,
4238 // NORMAL | TEXT_RCDATA
4239 'tr' => 1,
4240 'track' => 9,
4241 // NORMAL | VOID_TAG
4242 'u' => 1,
4243 'ul' => 81,
4244 // NORMAL | AUTOCLOSE_P | BLOCK_TAG
4245 'var' => 1,
4246 'video' => 65,
4247 // NORMAL | BLOCK_TAG
4248 'wbr' => 9,
4249 // NORMAL | VOID_TAG
4250 // Legacy?
4251 'basefont' => 8,
4252 // VOID_TAG
4253 'bgsound' => 8,
4254 // VOID_TAG
4255 'noframes' => 2,
4256 // RAW_TEXT
4257 'frame' => 9,
4258 // NORMAL | VOID_TAG
4259 'frameset' => 1,
4260 'center' => 16,
4261 'dir' => 16,
4262 'listing' => 16,
4263 // AUTOCLOSE_P
4264 'plaintext' => 48,
4265 // AUTOCLOSE_P | TEXT_PLAINTEXT
4266 'applet' => 0,
4267 'marquee' => 0,
4268 'isindex' => 8,
4269 // VOID_TAG
4270 'xmp' => 20,
4271 // AUTOCLOSE_P | VOID_TAG | RAW_TEXT
4272 'noembed' => 2,
4273 );
4274 /**
4275 * The MathML elements.
4276 * See http://www.w3.org/wiki/MathML/Elements.
4277 *
4278 * In our case we are only concerned with presentation MathML and not content
4279 * MathML. There is a nice list of this subset at https://developer.mozilla.org/en-US/docs/MathML/Element.
4280 *
4281 * @var array
4282 */
4283 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);
4284 /**
4285 * The svg elements.
4286 *
4287 * The Mozilla documentation has a good list at https://developer.mozilla.org/en-US/docs/SVG/Element.
4288 * The w3c list appears to be lacking in some areas like filter effect elements.
4289 * That list can be found at http://www.w3.org/wiki/SVG/Elements.
4290 *
4291 * Note, FireFox appears to do a better job rendering filter effects than chrome.
4292 * While they are in the spec I'm not sure how widely implemented they are.
4293 *
4294 * @var array
4295 */
4296 public static $svg = array(
4297 'a' => 1,
4298 'altGlyph' => 1,
4299 'altGlyphDef' => 1,
4300 'altGlyphItem' => 1,
4301 'animate' => 1,
4302 'animateColor' => 1,
4303 'animateMotion' => 1,
4304 'animateTransform' => 1,
4305 'circle' => 1,
4306 'clipPath' => 1,
4307 'color-profile' => 1,
4308 'cursor' => 1,
4309 'defs' => 1,
4310 'desc' => 1,
4311 'ellipse' => 1,
4312 'feBlend' => 1,
4313 'feColorMatrix' => 1,
4314 'feComponentTransfer' => 1,
4315 'feComposite' => 1,
4316 'feConvolveMatrix' => 1,
4317 'feDiffuseLighting' => 1,
4318 'feDisplacementMap' => 1,
4319 'feDistantLight' => 1,
4320 'feFlood' => 1,
4321 'feFuncA' => 1,
4322 'feFuncB' => 1,
4323 'feFuncG' => 1,
4324 'feFuncR' => 1,
4325 'feGaussianBlur' => 1,
4326 'feImage' => 1,
4327 'feMerge' => 1,
4328 'feMergeNode' => 1,
4329 'feMorphology' => 1,
4330 'feOffset' => 1,
4331 'fePointLight' => 1,
4332 'feSpecularLighting' => 1,
4333 'feSpotLight' => 1,
4334 'feTile' => 1,
4335 'feTurbulence' => 1,
4336 'filter' => 1,
4337 'font' => 1,
4338 'font-face' => 1,
4339 'font-face-format' => 1,
4340 'font-face-name' => 1,
4341 'font-face-src' => 1,
4342 'font-face-uri' => 1,
4343 'foreignObject' => 1,
4344 'g' => 1,
4345 'glyph' => 1,
4346 'glyphRef' => 1,
4347 'hkern' => 1,
4348 'image' => 1,
4349 'line' => 1,
4350 'linearGradient' => 1,
4351 'marker' => 1,
4352 'mask' => 1,
4353 'metadata' => 1,
4354 'missing-glyph' => 1,
4355 'mpath' => 1,
4356 'path' => 1,
4357 'pattern' => 1,
4358 'polygon' => 1,
4359 'polyline' => 1,
4360 'radialGradient' => 1,
4361 'rect' => 1,
4362 'script' => 3,
4363 // NORMAL | RAW_TEXT
4364 'set' => 1,
4365 'stop' => 1,
4366 'style' => 3,
4367 // NORMAL | RAW_TEXT
4368 'svg' => 1,
4369 'switch' => 1,
4370 'symbol' => 1,
4371 'text' => 1,
4372 'textPath' => 1,
4373 'title' => 1,
4374 'tref' => 1,
4375 'tspan' => 1,
4376 'use' => 1,
4377 'view' => 1,
4378 'vkern' => 1,
4379 );
4380 /**
4381 * Some attributes in SVG are case sensetitive.
4382 *
4383 * This map contains key/value pairs with the key as the lowercase attribute
4384 * name and the value with the correct casing.
4385 */
4386 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');
4387 /**
4388 * Some SVG elements are case sensetitive.
4389 * This map contains these.
4390 *
4391 * The map contains key/value store of the name is lowercase as the keys and
4392 * the correct casing as the value.
4393 */
4394 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');
4395 /**
4396 * Check whether the given element meets the given criterion.
4397 *
4398 * Example:
4399 *
4400 * Elements::isA('script', Elements::TEXT_RAW); // Returns true.
4401 *
4402 * Elements::isA('script', Elements::TEXT_RCDATA); // Returns false.
4403 *
4404 * @param string $name
4405 * The element name.
4406 * @param int $mask
4407 * One of the constants on this class.
4408 * @return boolean true if the element matches the mask, false otherwise.
4409 */
4410 public static function isA($name, $mask)
4411 {
4412 if (!static::isElement($name)) {
4413 return false;
4414 }
4415 return (static::element($name) & $mask) == $mask;
4416 }
4417 /**
4418 * Test if an element is a valid html5 element.
4419 *
4420 * @param string $name
4421 * The name of the element.
4422 *
4423 * @return bool True if a html5 element and false otherwise.
4424 */
4425 public static function isHtml5Element($name)
4426 {
4427 // html5 element names are case insensetitive. Forcing lowercase for the check.
4428 // Do we need this check or will all data passed here already be lowercase?
4429 return isset(static::$html5[strtolower($name)]);
4430 }
4431 /**
4432 * Test if an element name is a valid MathML presentation element.
4433 *
4434 * @param string $name
4435 * The name of the element.
4436 *
4437 * @return bool True if a MathML name and false otherwise.
4438 */
4439 public static function isMathMLElement($name)
4440 {
4441 // MathML is case-sensetitive unlike html5 elements.
4442 return isset(static::$mathml[$name]);
4443 }
4444 /**
4445 * Test if an element is a valid SVG element.
4446 *
4447 * @param string $name
4448 * The name of the element.
4449 *
4450 * @return boolean True if a SVG element and false otherise.
4451 */
4452 public static function isSvgElement($name)
4453 {
4454 // SVG is case-sensetitive unlike html5 elements.
4455 return isset(static::$svg[$name]);
4456 }
4457 /**
4458 * Is an element name valid in an html5 document.
4459 *
4460 * This includes html5 elements along with other allowed embedded content
4461 * such as svg and mathml.
4462 *
4463 * @param string $name
4464 * The name of the element.
4465 *
4466 * @return bool True if valid and false otherwise.
4467 */
4468 public static function isElement($name)
4469 {
4470 return static::isHtml5Element($name) || static::isMathMLElement($name) || static::isSvgElement($name);
4471 }
4472 /**
4473 * Get the element mask for the given element name.
4474 *
4475 * @param string $name
4476 * The name of the element.
4477 *
4478 * @return int|bool The element mask or false if element does not exist.
4479 */
4480 public static function element($name)
4481 {
4482 if (isset(static::$html5[$name])) {
4483 return static::$html5[$name];
4484 }
4485 if (isset(static::$svg[$name])) {
4486 return static::$svg[$name];
4487 }
4488 if (isset(static::$mathml[$name])) {
4489 return static::$mathml[$name];
4490 }
4491 return false;
4492 }
4493 /**
4494 * Normalize a SVG element name to its proper case and form.
4495 *
4496 * @param string $name
4497 * The name of the element.
4498 *
4499 * @return string The normalized form of the element name.
4500 */
4501 public static function normalizeSvgElement($name)
4502 {
4503 $name = strtolower($name);
4504 if (isset(static::$svgCaseSensitiveElementMap[$name])) {
4505 $name = static::$svgCaseSensitiveElementMap[$name];
4506 }
4507 return $name;
4508 }
4509 /**
4510 * Normalize a SVG attribute name to its proper case and form.
4511 *
4512 * @param string $name
4513 * The name of the attribute.
4514 *
4515 * @return string The normalized form of the attribute name.
4516 */
4517 public static function normalizeSvgAttribute($name)
4518 {
4519 $name = strtolower($name);
4520 if (isset(static::$svgCaseSensitiveAttributeMap[$name])) {
4521 $name = static::$svgCaseSensitiveAttributeMap[$name];
4522 }
4523 return $name;
4524 }
4525 /**
4526 * Normalize a MathML attribute name to its proper case and form.
4527 *
4528 * Note, all MathML element names are lowercase.
4529 *
4530 * @param string $name
4531 * The name of the attribute.
4532 *
4533 * @return string The normalized form of the attribute name.
4534 */
4535 public static function normalizeMathMlAttribute($name)
4536 {
4537 $name = strtolower($name);
4538 // Only one attribute has a mixed case form for MathML.
4539 if ($name == 'definitionurl') {
4540 $name = 'definitionURL';
4541 }
4542 return $name;
4543 }
4544 }
4545 namespace Kibo\Phast;
4546
4547 class PhastServices
4548 {
4549 /**
4550 * @param callable|null $getConfig
4551 */
4552 public static function serve(callable $getConfig = null)
4553 {
4554 $httpRequest = \Kibo\Phast\HTTP\Request::fromGlobals();
4555 if ($httpRequest->getHeader('CDN-Loop') && preg_match('~(^|,)\\s*Phast\\b~', $httpRequest->getHeader('CDN-Loop'))) {
4556 http_response_code(508);
4557 die('Loop detected');
4558 }
4559 $serviceRequest = \Kibo\Phast\Services\ServiceRequest::fromHTTPRequest($httpRequest);
4560 $serviceParams = $serviceRequest->getParams();
4561 if (defined('PHAST_SERVICE')) {
4562 $service = PHAST_SERVICE;
4563 } elseif (!isset($serviceParams['service'])) {
4564 http_response_code(404);
4565 exit;
4566 } else {
4567 $service = $serviceParams['service'];
4568 }
4569 if (isset($serviceParams['src']) && !headers_sent()) {
4570 if (self::isRewrittenRequest($httpRequest)) {
4571 http_response_code(500);
4572 } else {
4573 http_response_code(301);
4574 header('Location: ' . $serviceParams['src']);
4575 header('Cache-Control: max-age=86400');
4576 }
4577 }
4578 if ($getConfig === null) {
4579 $config = [];
4580 } else {
4581 $config = $getConfig();
4582 }
4583 $userConfig = new \Kibo\Phast\Environment\Configuration($config);
4584 $runtimeConfig = \Kibo\Phast\Environment\Configuration::fromDefaults()->withUserConfiguration($userConfig)->withServiceRequest($serviceRequest)->getRuntimeConfig()->toArray();
4585 \Kibo\Phast\Logging\Log::init($runtimeConfig['logging'], $serviceRequest, $service);
4586 try {
4587 \Kibo\Phast\Services\ServiceRequest::setDefaultSerializationMode($runtimeConfig['serviceRequestFormat']);
4588 \Kibo\Phast\Logging\Log::info('Starting service');
4589 $response = (new \Kibo\Phast\Services\Factory())->make($service, $runtimeConfig)->serve($serviceRequest);
4590 \Kibo\Phast\Logging\Log::info('Service completed');
4591 } catch (\Kibo\Phast\Exceptions\UnauthorizedException $e) {
4592 echo "Unauthorized\n";
4593 \Kibo\Phast\Logging\Log::error('Unauthorized exception: {message}', ['message' => $e->getMessage()]);
4594 exit;
4595 } catch (\Kibo\Phast\Exceptions\ItemNotFoundException $e) {
4596 echo "Item not found\n";
4597 \Kibo\Phast\Logging\Log::error('Item not found: {message}', ['message' => $e->getMessage()]);
4598 exit;
4599 } catch (\Exception $e) {
4600 echo "Internal error, see logs\n";
4601 \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()]);
4602 exit;
4603 }
4604 header_remove('Location');
4605 header_remove('Cache-Control');
4606 self::output($httpRequest, $response, $runtimeConfig);
4607 }
4608 public static function isRewrittenRequest()
4609 {
4610 return !!\Kibo\Phast\Services\ServiceRequest::getRewrittenService(\Kibo\Phast\HTTP\Request::fromGlobals());
4611 }
4612 public static function output(\Kibo\Phast\HTTP\Request $request, \Kibo\Phast\HTTP\Response $response, array $config, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
4613 {
4614 if (is_null($funcs)) {
4615 $funcs = new \Kibo\Phast\Common\ObjectifiedFunctions();
4616 }
4617 $headers = $response->getHeaders();
4618 $content = $response->getContent();
4619 if (!self::isIterable($content)) {
4620 $content = [$content];
4621 }
4622 $fp = fopen('php://output', 'wb');
4623 $zipping = false;
4624 if ($response->isCompressible() && self::shouldZip($request) && !empty($config['compressServiceResponse'])) {
4625 $zipping = @$funcs->stream_filter_append($fp, 'zlib.deflate', STREAM_FILTER_WRITE, ['level' => 9, 'window' => 31]);
4626 if ($zipping) {
4627 $headers['Content-Encoding'] = 'gzip';
4628 }
4629 }
4630 $maxAge = 86400 * 365;
4631 $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'"];
4632 if (is_array($content) && !$zipping) {
4633 $headers['Content-Length'] = (string) array_sum(array_map('strlen', $content));
4634 }
4635 $funcs->http_response_code($response->getCode());
4636 foreach ($headers as $name => $value) {
4637 $funcs->header($name . ': ' . $value);
4638 }
4639 foreach ($content as $part) {
4640 fwrite($fp, $part);
4641 }
4642 fclose($fp);
4643 }
4644 private static function formatHeaderDate($time)
4645 {
4646 return gmdate('D, d M Y H:i:s', $time) . ' GMT';
4647 }
4648 private static function shouldZip(\Kibo\Phast\HTTP\Request $request)
4649 {
4650 return !$request->isCloudflare() && strpos($request->getHeader('Accept-Encoding'), 'gzip') !== false;
4651 }
4652 private static function generateETag(array $headers, $content)
4653 {
4654 $headersPart = http_build_query($headers);
4655 $contentPart = self::isIterable($content) ? uniqid() : $content;
4656 return '"' . md5($headersPart . "\0" . $contentPart) . '"';
4657 }
4658 private static function isIterable($thing)
4659 {
4660 return is_array($thing) || $thing instanceof \Iterator || $thing instanceof \Generator;
4661 }
4662 }
4663 namespace Kibo\Phast\Common;
4664
4665 class JSON
4666 {
4667 public static function encode($value)
4668 {
4669 return self::_encode($value, 0);
4670 }
4671 public static function prettyEncode($value)
4672 {
4673 return self::_encode($value, JSON_PRETTY_PRINT);
4674 }
4675 private static function _encode($value, $flags)
4676 {
4677 $flags |= JSON_UNESCAPED_SLASHES;
4678 if (version_compare(PHP_VERSION, '7.2.0', '<')) {
4679 return self::legacyEncode($value, $flags);
4680 }
4681 return json_encode($value, $flags | JSON_INVALID_UTF8_IGNORE | JSON_PARTIAL_OUTPUT_ON_ERROR);
4682 }
4683 private static function legacyEncode($value, $flags)
4684 {
4685 $result = json_encode($value, $flags);
4686 if ($result !== false || json_last_error() !== JSON_ERROR_UTF8) {
4687 return $result;
4688 }
4689 self::cleanUTF8($value);
4690 return json_encode($value, $flags | JSON_PARTIAL_OUTPUT_ON_ERROR);
4691 }
4692 private static function cleanUTF8(&$value)
4693 {
4694 if (is_array($value)) {
4695 array_walk_recursive($value, __METHOD__);
4696 } elseif (is_string($value)) {
4697 $value = preg_replace_callback('~
4698 [\\x00-\\x7F]++ # ASCII
4699 | [\\xC2-\\xDF][\\x80-\\xBF] # non-overlong 2-byte
4700 | \\xE0[\\xA0-\\xBF][\\x80-\\xBF] # excluding overlongs
4701 | [\\xE1-\\xEC\\xEE\\xEF][\\x80-\\xBF]{2} # straight 3-byte
4702 | \\xED[\\x80-\\x9F][\\x80-\\xBF] # excluding surrogates
4703 | \\xF0[\\x90-\\xBF][\\x80-\\xBF]{2} # planes 1-3
4704 | [\\xF1-\\xF3][\\x80-\\xBF]{3} # planes 4-15
4705 | \\xF4[\\x80-\\x8F][\\x80-\\xBF]{2} # plane 16
4706 | (.)
4707 ~xs', function ($match) {
4708 if (isset($match[1]) && strlen($match[1])) {
4709 return '';
4710 }
4711 return $match[0];
4712 }, $value);
4713 }
4714 }
4715 }
4716 namespace Kibo\Phast\Common;
4717
4718 class Base64url
4719 {
4720 public static function encode($data)
4721 {
4722 return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
4723 }
4724 public static function decode($data)
4725 {
4726 return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
4727 }
4728 public static function shortHash($data)
4729 {
4730 return self::encode(substr(sha1($data, true), 0, 8));
4731 }
4732 }
4733 namespace Kibo\Phast\Common;
4734
4735 class ObjectifiedFunctions
4736 {
4737 /**
4738 * @param string $name
4739 * @param array $arguments
4740 */
4741 public function __call($name, array $arguments)
4742 {
4743 if (isset($this->{$name}) && is_callable($this->{$name})) {
4744 $fn = $this->{$name};
4745 return $fn(...$arguments);
4746 }
4747 if (function_exists($name)) {
4748 return $name(...$arguments);
4749 }
4750 throw new \Kibo\Phast\Exceptions\UndefinedObjectifiedFunction("Undefined objectified function {$name}");
4751 }
4752 }
4753 namespace Kibo\Phast\Common;
4754
4755 class System
4756 {
4757 private $functions;
4758 public function __construct(\Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
4759 {
4760 if ($functions === null) {
4761 $functions = new \Kibo\Phast\Common\ObjectifiedFunctions();
4762 }
4763 $this->functions = $functions;
4764 }
4765 public function getUserId()
4766 {
4767 try {
4768 return (int) $this->functions->posix_geteuid();
4769 } catch (\Kibo\Phast\Exceptions\UndefinedObjectifiedFunction $e) {
4770 return 0;
4771 }
4772 }
4773 }
4774 namespace Kibo\Phast\Common;
4775
4776 class JSMinifier extends \Kibo\Phast\JSMin\JSMin
4777 {
4778 protected $removeLicenseHeaders;
4779 public function __construct($input, $removeLicenseHeaders = false)
4780 {
4781 parent::__construct($input);
4782 $this->removeLicenseHeaders = $removeLicenseHeaders;
4783 }
4784 protected function consumeMultipleLineComment()
4785 {
4786 parent::consumeMultipleLineComment();
4787 if ($this->removeLicenseHeaders) {
4788 $this->keptComment = preg_replace('~/\\*!.*?\\*/~s', '', $this->keptComment);
4789 }
4790 }
4791 }
4792 namespace Kibo\Phast\Common;
4793
4794 class OutputBufferHandler
4795 {
4796 use \Kibo\Phast\Logging\LoggingTrait;
4797 const START_PATTERN = '~
4798 (
4799 \\s*+ <!doctype\\s++html> |
4800 \\s*+ <html> |
4801 \\s*+ <head> |
4802 \\s*+ <!--.*?-->
4803 )++
4804 ~xsiA';
4805 private $filterCb;
4806 /**
4807 * @var ?string
4808 */
4809 private $buffer = '';
4810 private $offset = 0;
4811 /**
4812 * @var integer
4813 */
4814 private $maxBufferSizeToApply;
4815 private $canceled = false;
4816 public function __construct($maxBufferSizeToApply, callable $filterCb)
4817 {
4818 $this->maxBufferSizeToApply = $maxBufferSizeToApply;
4819 $this->filterCb = $filterCb;
4820 }
4821 public function install()
4822 {
4823 $ignoreHandlers = ['default output handler', 'ob_gzhandler'];
4824 if (!array_diff(ob_list_handlers(), $ignoreHandlers)) {
4825 while (@ob_end_clean()) {
4826 }
4827 }
4828 ob_start([$this, 'handleChunk'], 2);
4829 ob_implicit_flush(1);
4830 }
4831 public function handleChunk($chunk, $phase)
4832 {
4833 if ($this->buffer === null) {
4834 return $chunk;
4835 }
4836 $this->buffer .= $chunk;
4837 if ($this->canceled) {
4838 return $this->stop();
4839 }
4840 if (strlen($this->buffer) > $this->maxBufferSizeToApply) {
4841 $this->logger()->info('Buffer exceeds max. size ({buffersize} bytes). Not applying', ['buffersize' => $this->maxBufferSizeToApply]);
4842 return $this->stop();
4843 }
4844 $output = '';
4845 if (preg_match(self::START_PATTERN, $this->buffer, $match, 0, $this->offset)) {
4846 $this->offset += strlen($match[0]);
4847 $output .= $match[0];
4848 }
4849 if ($phase & PHP_OUTPUT_HANDLER_FINAL) {
4850 $output .= $this->finalize();
4851 }
4852 if ($output !== '') {
4853 @header_remove('Content-Length');
4854 }
4855 return $output;
4856 }
4857 private function finalize()
4858 {
4859 $input = substr($this->buffer, $this->offset);
4860 $result = call_user_func($this->filterCb, $input, $this->buffer);
4861 $this->buffer = null;
4862 return $result;
4863 }
4864 private function stop()
4865 {
4866 $output = $this->buffer;
4867 $this->buffer = null;
4868 return $output;
4869 }
4870 public function cancel()
4871 {
4872 $this->canceled = true;
4873 }
4874 }
4875 namespace Kibo\Phast\ValueObjects;
4876
4877 class PhastJavaScript
4878 {
4879 /**
4880 * @var string
4881 */
4882 private $filename;
4883 /**
4884 * @var string
4885 */
4886 private $contents;
4887 /**
4888 * @var string
4889 */
4890 private $configKey;
4891 /**
4892 * @var mixed
4893 */
4894 private $config;
4895 /**
4896 * @var ObjectifiedFunctions
4897 */
4898 private $funcs;
4899 /**
4900 * @param string $filename
4901 * @param string $contents
4902 */
4903 private function __construct($filename, $contents)
4904 {
4905 $this->filename = $filename;
4906 $this->contents = $contents;
4907 }
4908 /**
4909 * @param string $filename
4910 * @param ObjectifiedFunctions|null $funcs
4911 */
4912 public static function fromFile($filename, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
4913 {
4914 $funcs = $funcs ? $funcs : new \Kibo\Phast\Common\ObjectifiedFunctions();
4915 $contents = $funcs->file_get_contents($filename);
4916 if ($contents === false) {
4917 throw new \RuntimeException("Failed to read script: {$filename}");
4918 }
4919 $contents = (new \Kibo\Phast\Common\JSMinifier($contents))->min();
4920 return new self($filename, $contents);
4921 }
4922 /**
4923 * @param string $filename
4924 * @param string $contents
4925 */
4926 public static function fromString($filename, $contents)
4927 {
4928 return new self($filename, $contents);
4929 }
4930 /**
4931 * @return string
4932 */
4933 public function getFilename()
4934 {
4935 return $this->filename;
4936 }
4937 /**
4938 * @return bool|string
4939 */
4940 public function getContents()
4941 {
4942 return $this->contents;
4943 }
4944 /**
4945 * @return string
4946 */
4947 public function getCacheSalt()
4948 {
4949 $hash = md5($this->getContents(), true);
4950 return substr(preg_replace('/^[a-z0-9]/i', '', base64_encode($hash)), 0, 16);
4951 }
4952 /**
4953 * @param string $configKey
4954 * @param mixed $config
4955 */
4956 public function setConfig($configKey, $config)
4957 {
4958 $this->configKey = $configKey;
4959 $this->config = $config;
4960 }
4961 /**
4962 * @return bool
4963 */
4964 public function hasConfig()
4965 {
4966 return isset($this->configKey);
4967 }
4968 /**
4969 * @return string
4970 */
4971 public function getConfigKey()
4972 {
4973 return $this->configKey;
4974 }
4975 /**
4976 * @return mixed
4977 */
4978 public function getConfig()
4979 {
4980 return $this->config;
4981 }
4982 }
4983 namespace Kibo\Phast\ValueObjects;
4984
4985 class Resource
4986 {
4987 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');
4988 /**
4989 * @var URL
4990 */
4991 private $url;
4992 /**
4993 * @var Retriever
4994 */
4995 private $retriever;
4996 /**
4997 * @var string
4998 */
4999 private $content;
5000 /**
5001 * @var string
5002 */
5003 private $mimeType;
5004 /**
5005 * @var Resource[]
5006 */
5007 private $dependencies = array();
5008 private function __construct()
5009 {
5010 }
5011 public static function makeWithContent(\Kibo\Phast\ValueObjects\URL $url, $content, $mimeType = null)
5012 {
5013 $instance = new self();
5014 $instance->url = $url;
5015 $instance->mimeType = $mimeType;
5016 $instance->content = $content;
5017 return $instance;
5018 }
5019 public static function makeWithRetriever(\Kibo\Phast\ValueObjects\URL $url, \Kibo\Phast\Retrievers\Retriever $retriever, $mimeType = null)
5020 {
5021 $instance = new self();
5022 $instance->url = $url;
5023 $instance->mimeType = $mimeType;
5024 $instance->retriever = $retriever;
5025 return $instance;
5026 }
5027 /**
5028 * @return URL
5029 */
5030 public function getUrl()
5031 {
5032 return $this->url;
5033 }
5034 /**
5035 * @return string
5036 * @throws ItemNotFoundException
5037 */
5038 public function getContent()
5039 {
5040 if (!isset($this->content)) {
5041 $this->content = $this->retriever->retrieve($this->url);
5042 if ($this->content === false) {
5043 throw new \Kibo\Phast\Exceptions\ItemNotFoundException("Could not get {$this->url}");
5044 }
5045 }
5046 return $this->content;
5047 }
5048 /**
5049 * @return string|null
5050 */
5051 public function getMimeType()
5052 {
5053 if (!isset($this->mimeType)) {
5054 $ext = strtolower($this->url->getExtension());
5055 $ext2mime = self::EXTENSION_TO_MIME_TYPE;
5056 if (isset($ext2mime[$ext])) {
5057 $this->mimeType = self::EXTENSION_TO_MIME_TYPE[$ext];
5058 }
5059 }
5060 return $this->mimeType;
5061 }
5062 /**
5063 * @return bool|int
5064 */
5065 public function getSize()
5066 {
5067 if (isset($this->retriever) && method_exists($this->retriever, 'getSize')) {
5068 return $this->retriever->getSize($this->url);
5069 }
5070 if (isset($this->content)) {
5071 return strlen($this->content);
5072 }
5073 return false;
5074 }
5075 public function toDataURL()
5076 {
5077 $mime = $this->getMimeType();
5078 $content = $this->getContent();
5079 return "data:{$mime};base64," . base64_encode($content);
5080 }
5081 /**
5082 * @return Resource[]
5083 */
5084 public function getDependencies()
5085 {
5086 return $this->dependencies;
5087 }
5088 /**
5089 * @return bool|int
5090 */
5091 public function getCacheSalt()
5092 {
5093 return isset($this->retriever) ? $this->retriever->getCacheSalt($this->url) : 0;
5094 }
5095 /**
5096 * @param string $content
5097 * @param string|null $mimeType
5098 * @return Resource
5099 */
5100 public function withContent($content, $mimeType = null)
5101 {
5102 $new = clone $this;
5103 $new->content = $content;
5104 if (!is_null($mimeType)) {
5105 $new->mimeType = $mimeType;
5106 }
5107 return $new;
5108 }
5109 /**
5110 * @param Resource[] $dependencies
5111 * @return Resource
5112 */
5113 public function withDependencies(array $dependencies)
5114 {
5115 $new = clone $this;
5116 $new->dependencies = $dependencies;
5117 return $new;
5118 }
5119 }
5120 namespace Kibo\Phast\ValueObjects;
5121
5122 class Query implements \IteratorAggregate
5123 {
5124 private $tuples = array();
5125 /**
5126 * @param array $assoc
5127 * @return Query
5128 */
5129 public static function fromAssoc($assoc)
5130 {
5131 $result = new static();
5132 foreach ($assoc as $k => $v) {
5133 $result->add($k, $v);
5134 }
5135 return $result;
5136 }
5137 /**
5138 * @param string $string
5139 * @return Query
5140 */
5141 public static function fromString($string)
5142 {
5143 $result = new static();
5144 foreach (explode('&', $string) as $piece) {
5145 if ($piece === '') {
5146 continue;
5147 }
5148 $parts = array_map('urldecode', explode('=', $piece, 2));
5149 $result->add($parts[0], isset($parts[1]) ? $parts[1] : '');
5150 }
5151 return $result;
5152 }
5153 public function add($key, $value)
5154 {
5155 $this->tuples[] = [(string) $key, (string) $value];
5156 }
5157 public function get($key, $default = null)
5158 {
5159 foreach ($this->tuples as $tuple) {
5160 if ($tuple[0] === (string) $key) {
5161 return $tuple[1];
5162 }
5163 }
5164 return $default;
5165 }
5166 public function delete($key)
5167 {
5168 $this->tuples = array_filter($this->tuples, function ($tuple) use($key) {
5169 return $tuple[0] !== (string) $key;
5170 });
5171 }
5172 public function set($key, $value)
5173 {
5174 $this->delete($key);
5175 $this->add($key, $value);
5176 }
5177 public function has($key)
5178 {
5179 foreach ($this->tuples as $tuple) {
5180 if ($tuple[0] === (string) $key) {
5181 return true;
5182 }
5183 }
5184 return false;
5185 }
5186 public function update(\Kibo\Phast\ValueObjects\Query $source)
5187 {
5188 foreach ($source as $key => $value) {
5189 $this->delete($key);
5190 }
5191 foreach ($source as $key => $value) {
5192 $this->add($key, $value);
5193 }
5194 }
5195 public function toAssoc()
5196 {
5197 $assoc = [];
5198 foreach ($this->tuples as $tuple) {
5199 if (!array_key_exists($tuple[0], $assoc)) {
5200 $assoc[$tuple[0]] = $tuple[1];
5201 }
5202 }
5203 return $assoc;
5204 }
5205 public function getIterator()
5206 {
5207 foreach ($this->tuples as $tuple) {
5208 (yield $tuple[0] => $tuple[1]);
5209 }
5210 }
5211 public function pop($key)
5212 {
5213 $value = $this->get($key);
5214 $this->delete($key);
5215 return $value;
5216 }
5217 public function getAll($key)
5218 {
5219 $result = [];
5220 foreach ($this->tuples as $tuple) {
5221 if ($tuple[0] === (string) $key) {
5222 $result[] = $tuple[1];
5223 }
5224 }
5225 return $result;
5226 }
5227 }
5228 namespace Kibo\Phast\ValueObjects;
5229
5230 class URL
5231 {
5232 /**
5233 * @var string
5234 */
5235 private $scheme;
5236 /**
5237 * @var string
5238 */
5239 private $host;
5240 /**
5241 * @var string
5242 */
5243 private $port;
5244 /**
5245 * @var string
5246 */
5247 private $user;
5248 /**
5249 * @var string
5250 */
5251 private $pass;
5252 /**
5253 * @var string
5254 */
5255 private $path;
5256 /**
5257 * @var string
5258 */
5259 private $query;
5260 /**
5261 * @var string
5262 */
5263 private $fragment;
5264 /**
5265 * @param $string
5266 * @return URL
5267 */
5268 public static function fromString($string)
5269 {
5270 $components = parse_url($string);
5271 if (!$components) {
5272 return new self();
5273 }
5274 return self::fromArray($components);
5275 }
5276 /**
5277 * @param array $arr Should follow the format produced by parse_url()
5278 * @return URL
5279 * @see parse_url()
5280 */
5281 public static function fromArray(array $arr)
5282 {
5283 $url = new self();
5284 foreach ($arr as $key => $value) {
5285 $url->{$key} = $key == 'path' ? $url->normalizePath($value) : $value;
5286 }
5287 return $url;
5288 }
5289 /**
5290 * If $this can be interpreted as relative to $base,
5291 * will produce URL that is $base/$this.
5292 * Otherwise the returned URL will point to the same place as $this
5293 *
5294 * @param URL $base
5295 * @return URL
5296 *
5297 * @example this: www/htdocs + base: /var -> /var/www/htdocs
5298 * @example this: /var + base: http://example.com -> http://example.com/var
5299 * @example this: /var + base: /www -> /var
5300 */
5301 public function withBase(\Kibo\Phast\ValueObjects\URL $base)
5302 {
5303 $new = clone $this;
5304 foreach (['scheme', 'host', 'port', 'user', 'pass', 'path'] as $key) {
5305 if ($key == 'path') {
5306 $new->path = $this->resolvePath($base->path, $this->path);
5307 } elseif (!isset($this->{$key}) && isset($base->{$key})) {
5308 $new->{$key} = $base->{$key};
5309 } elseif (isset($this->{$key})) {
5310 break;
5311 }
5312 }
5313 return $new;
5314 }
5315 /**
5316 * Tells whether $this can be interpreted as at the same host as $url
5317 *
5318 * @param URL $url
5319 * @return bool
5320 */
5321 public function isLocalTo(\Kibo\Phast\ValueObjects\URL $url)
5322 {
5323 return empty($this->host) || $this->host === $url->host;
5324 }
5325 /**
5326 * @return string
5327 */
5328 public function toString()
5329 {
5330 $scheme = isset($this->scheme) ? $this->scheme . '://' : '';
5331 $host = isset($this->host) ? $this->host : '';
5332 $port = isset($this->port) ? ':' . $this->port : '';
5333 $user = isset($this->user) ? $this->user : '';
5334 $pass = isset($this->pass) ? ':' . $this->pass : '';
5335 $pass = $user || $pass ? "{$pass}@" : '';
5336 $path = isset($this->path) ? $this->getPath() : '';
5337 $query = isset($this->query) ? '?' . $this->query : '';
5338 $fragment = isset($this->fragment) ? '#' . $this->fragment : '';
5339 return "{$scheme}{$user}{$pass}{$host}{$port}{$path}{$query}{$fragment}";
5340 }
5341 private function normalizePath($path)
5342 {
5343 $stack = [];
5344 $head = null;
5345 foreach (explode('/', $path) as $part) {
5346 if ($part == '.' || $part == '') {
5347 continue;
5348 }
5349 if (!is_null($head) && $part == '..' && $head != '..') {
5350 array_pop($stack);
5351 $head = empty($stack) ? null : $stack[count($stack) - 1];
5352 } else {
5353 $stack[] = $head = $part;
5354 }
5355 }
5356 $normalized = substr($path, 0, 1) == '/' ? '/' : '';
5357 if (!empty($stack)) {
5358 $normalized .= join('/', $stack);
5359 $normalized .= substr($path, -1) == '/' ? '/' : '';
5360 }
5361 return $normalized;
5362 }
5363 private function resolvePath($base, $requested)
5364 {
5365 if (!$requested) {
5366 return $base;
5367 }
5368 if ($requested[0] == '/') {
5369 return $requested;
5370 }
5371 if (substr($base, -1, 1) == '/') {
5372 $usedBase = $base;
5373 } else {
5374 $usedBase = dirname($base);
5375 }
5376 return rtrim($usedBase, '/') . '/' . $requested;
5377 }
5378 /**
5379 * @return string
5380 */
5381 public function getScheme()
5382 {
5383 return $this->scheme;
5384 }
5385 /**
5386 * @return string
5387 */
5388 public function getHost()
5389 {
5390 return $this->host;
5391 }
5392 /**
5393 * @return string
5394 */
5395 public function getPort()
5396 {
5397 return $this->port;
5398 }
5399 /**
5400 * @return string
5401 */
5402 public function getUser()
5403 {
5404 return $this->user;
5405 }
5406 /**
5407 * @return string
5408 */
5409 public function getPass()
5410 {
5411 return $this->pass;
5412 }
5413 /** @return string */
5414 public function getDecodedPath()
5415 {
5416 return urldecode($this->path);
5417 }
5418 /**
5419 * @return string
5420 */
5421 public function getPath()
5422 {
5423 return $this->path;
5424 }
5425 /**
5426 * @return string
5427 */
5428 public function getQuery()
5429 {
5430 return $this->query;
5431 }
5432 /**
5433 * @return string
5434 */
5435 public function getExtension()
5436 {
5437 $matches = [];
5438 if (preg_match('/\\.([^.]*)$/', $this->path, $matches)) {
5439 return $matches[1];
5440 }
5441 return '';
5442 }
5443 /**
5444 * @return string
5445 */
5446 public function getFragment()
5447 {
5448 return $this->fragment;
5449 }
5450 /**
5451 * @param string $path
5452 * @return self
5453 */
5454 public function withPath($path)
5455 {
5456 $url = clone $this;
5457 $url->path = (string) $path;
5458 return $url;
5459 }
5460 /**
5461 * @param string|null $query
5462 * @return self
5463 */
5464 public function withQuery($query)
5465 {
5466 $url = clone $this;
5467 if ($query === null) {
5468 $url->query = null;
5469 } else {
5470 $url->query = (string) $query;
5471 }
5472 return $url;
5473 }
5474 /**
5475 * @return self
5476 */
5477 public function withoutQuery()
5478 {
5479 $url = clone $this;
5480 $url->query = null;
5481 return $url;
5482 }
5483 public function __toString()
5484 {
5485 return $this->toString();
5486 }
5487 public function rewrite(\Kibo\Phast\ValueObjects\URL $from, \Kibo\Phast\ValueObjects\URL $to)
5488 {
5489 $str_from = rtrim($from->toString(), '/');
5490 $str_to = rtrim($to->toString(), '/');
5491 return \Kibo\Phast\ValueObjects\URL::fromString(preg_replace('~^' . preg_quote($str_from, '~') . '(?=$|/)~', $str_to, $this->toString()));
5492 }
5493 }
5494 namespace Kibo\Phast\Logging;
5495
5496 class LogLevel
5497 {
5498 const EMERGENCY = 128;
5499 const ALERT = 64;
5500 const CRITICAL = 32;
5501 const ERROR = 16;
5502 const WARNING = 8;
5503 const NOTICE = 4;
5504 const INFO = 2;
5505 const DEBUG = 1;
5506 public static function toString($level)
5507 {
5508 switch ($level) {
5509 case self::EMERGENCY:
5510 return 'EMERGENCY';
5511 case self::ALERT:
5512 return 'ALERT';
5513 case self::CRITICAL:
5514 return 'CRITICAL';
5515 case self::ERROR:
5516 return 'ERROR';
5517 case self::WARNING:
5518 return 'WARNING';
5519 case self::NOTICE:
5520 return 'NOTICE';
5521 case self::INFO:
5522 return 'INFO';
5523 case self::DEBUG:
5524 return 'DEBUG';
5525 default:
5526 return 'UNKNOWN';
5527 }
5528 }
5529 }
5530 namespace Kibo\Phast\Logging;
5531
5532 class Log
5533 {
5534 /**
5535 * @var Logger
5536 */
5537 private static $logger;
5538 public static function setLogger(\Kibo\Phast\Logging\Logger $logger)
5539 {
5540 self::$logger = $logger;
5541 }
5542 public static function initWithDummy()
5543 {
5544 self::$logger = new \Kibo\Phast\Logging\Logger(new \Kibo\Phast\Logging\LogWriters\Dummy\Writer());
5545 }
5546 public static function init(array $config, \Kibo\Phast\Services\ServiceRequest $request, $service)
5547 {
5548 $writer = (new \Kibo\Phast\Logging\LogWriters\Factory())->make($config, $request);
5549 $logger = new \Kibo\Phast\Logging\Logger($writer);
5550 self::$logger = $logger->withContext(['documentRequestId' => $request->getDocumentRequestId(), 'requestId' => mt_rand(0, 99999999), 'service' => $service]);
5551 }
5552 /**
5553 * @return Logger
5554 */
5555 public static function get()
5556 {
5557 if (!isset(self::$logger)) {
5558 self::initWithDummy();
5559 }
5560 return self::$logger;
5561 }
5562 /**
5563 * @param array $context
5564 * @return Logger
5565 */
5566 public static function context(array $context)
5567 {
5568 return self::get()->withContext($context);
5569 }
5570 /**
5571 * System is unusable.
5572 *
5573 * @param string $message
5574 * @param array $context
5575 *
5576 * @return void
5577 */
5578 public static function emergency($message, array $context = array())
5579 {
5580 self::get()->emergency($message, $context);
5581 }
5582 /**
5583 * Action must be taken immediately.
5584 *
5585 * Example: Entire website down, database unavailable, etc. This should
5586 * trigger the SMS alerts and wake you up.
5587 *
5588 * @param string $message
5589 * @param array $context
5590 *
5591 * @return void
5592 */
5593 public static function alert($message, array $context = array())
5594 {
5595 self::get()->alert($message, $context);
5596 }
5597 /**
5598 * Critical conditions.
5599 *
5600 * Example: Application component unavailable, unexpected exception.
5601 *
5602 * @param string $message
5603 * @param array $context
5604 *
5605 * @return void
5606 */
5607 public static function critical($message, array $context = array())
5608 {
5609 self::get()->critical($message, $context);
5610 }
5611 /**
5612 * Runtime errors that do not require immediate action but should typically
5613 * be logged and monitored.
5614 *
5615 * @param string $message
5616 * @param array $context
5617 *
5618 * @return void
5619 */
5620 public static function error($message, array $context = array())
5621 {
5622 self::get()->error($message, $context);
5623 }
5624 /**
5625 * Exceptional occurrences that are not errors.
5626 *
5627 * Example: Use of deprecated APIs, poor use of an API, undesirable things
5628 * that are not necessarily wrong.
5629 *
5630 * @param string $message
5631 * @param array $context
5632 *
5633 * @return void
5634 */
5635 public static function warning($message, array $context = array())
5636 {
5637 self::get()->warning($message, $context);
5638 }
5639 /**
5640 * Normal but significant events.
5641 *
5642 * @param string $message
5643 * @param array $context
5644 *
5645 * @return void
5646 */
5647 public static function notice($message, array $context = array())
5648 {
5649 self::get()->notice($message, $context);
5650 }
5651 /**
5652 * Interesting events.
5653 *
5654 * Example: User logs in, SQL logs.
5655 *
5656 * @param string $message
5657 * @param array $context
5658 *
5659 * @return void
5660 */
5661 public static function info($message, array $context = array())
5662 {
5663 self::get()->info($message, $context);
5664 }
5665 /**
5666 * Detailed debug information.
5667 *
5668 * @param string $message
5669 * @param array $context
5670 *
5671 * @return void
5672 */
5673 public static function debug($message, array $context = array())
5674 {
5675 self::get()->debug($message, $context);
5676 }
5677 }
5678 namespace Kibo\Phast\Logging\LogWriters;
5679
5680 class Factory
5681 {
5682 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5683 {
5684 if (isset($config['logWriters']) && count($config['logWriters']) > 1) {
5685 $class = \Kibo\Phast\Logging\LogWriters\Composite\Writer::class;
5686 } elseif (isset($config['logWriters'])) {
5687 $config = array_pop($config['logWriters']);
5688 $class = $config['class'];
5689 } else {
5690 $class = $config['class'];
5691 }
5692 $package = \Kibo\Phast\Environment\Package::fromPackageClass($class);
5693 $writer = $package->getFactory()->make($config, $request);
5694 if (isset($config['levelMask'])) {
5695 $writer->setLevelMask($config['levelMask']);
5696 }
5697 return $writer;
5698 }
5699 }
5700 namespace Kibo\Phast\Logging\LogWriters\JSONLFile;
5701
5702 class Factory
5703 {
5704 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5705 {
5706 return new \Kibo\Phast\Logging\LogWriters\JSONLFile\Writer($config['logRoot'], $request->getDocumentRequestId());
5707 }
5708 }
5709 namespace Kibo\Phast\Logging\LogWriters\Composite;
5710
5711 class Factory
5712 {
5713 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5714 {
5715 $writer = new \Kibo\Phast\Logging\LogWriters\Composite\Writer();
5716 $factory = new \Kibo\Phast\Logging\LogWriters\Factory();
5717 foreach ($config['logWriters'] as $writerConfig) {
5718 $writer->addWriter($factory->make($writerConfig, $request));
5719 }
5720 return $writer;
5721 }
5722 }
5723 namespace Kibo\Phast\Logging\LogWriters\RotatingTextFile;
5724
5725 class Factory
5726 {
5727 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5728 {
5729 return new \Kibo\Phast\Logging\LogWriters\RotatingTextFile\Writer($config);
5730 }
5731 }
5732 namespace Kibo\Phast\Logging\LogWriters\PHPError;
5733
5734 class Factory
5735 {
5736 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5737 {
5738 return new \Kibo\Phast\Logging\LogWriters\PHPError\Writer($config);
5739 }
5740 }
5741 namespace Kibo\Phast\Logging\Common;
5742
5743 trait JSONLFileLogTrait
5744 {
5745 /**
5746 * @var string
5747 */
5748 private $dir;
5749 /**
5750 * @var string
5751 */
5752 private $filename;
5753 /**
5754 * JSONLFileLogWriter constructor.
5755 * @param string $dir
5756 * @param string $suffix
5757 */
5758 public function __construct($dir, $suffix)
5759 {
5760 $this->dir = $dir;
5761 $suffix = preg_replace('/[^0-9A-Za-z_-]/', '', (string) $suffix);
5762 if (!empty($suffix)) {
5763 $suffix = '-' . $suffix;
5764 }
5765 $this->filename = $this->dir . '/log' . $suffix . '.jsonl';
5766 }
5767 }
5768 namespace Kibo\Phast\Logging;
5769
5770 class LogEntry implements \JsonSerializable
5771 {
5772 /**
5773 * @var int
5774 */
5775 private $level;
5776 /**
5777 * @var string
5778 */
5779 private $message;
5780 /**
5781 * @var array
5782 */
5783 private $context;
5784 /**
5785 * LogEntry constructor.
5786 * @param int $level
5787 * @param string $message
5788 * @param array $context
5789 */
5790 public function __construct($level, $message, array $context)
5791 {
5792 $this->level = (int) $level;
5793 $this->message = $message;
5794 $this->context = $context;
5795 }
5796 /**
5797 * @return int
5798 */
5799 public function getLevel()
5800 {
5801 return $this->level;
5802 }
5803 /**
5804 * @return string
5805 */
5806 public function getMessage()
5807 {
5808 return $this->message;
5809 }
5810 /**
5811 * @return array
5812 */
5813 public function getContext()
5814 {
5815 return $this->context;
5816 }
5817 public function toArray()
5818 {
5819 return ['level' => $this->level, 'message' => $this->message, 'context' => $this->context];
5820 }
5821 public function jsonSerialize()
5822 {
5823 return $this->toArray();
5824 }
5825 }
5826 namespace Kibo\Phast\Logging;
5827
5828 class Logger
5829 {
5830 /**
5831 * @var LogWriter
5832 */
5833 private $writer;
5834 /**
5835 * @var array
5836 */
5837 private $context = array();
5838 /**
5839 * @var ObjectifiedFunctions
5840 */
5841 private $functions;
5842 /**
5843 * Logger constructor.
5844 * @param LogWriter $writer
5845 */
5846 public function __construct(\Kibo\Phast\Logging\LogWriter $writer, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
5847 {
5848 $this->writer = $writer;
5849 $this->functions = is_null($functions) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $functions;
5850 }
5851 /**
5852 * Returns a new logger with default context
5853 * merged from the current logger and the passed array
5854 *
5855 * @param array $context
5856 * @return Logger
5857 */
5858 public function withContext(array $context)
5859 {
5860 $logger = clone $this;
5861 $logger->context = array_merge($this->context, $context);
5862 return $logger;
5863 }
5864 /**
5865 * System is unusable.
5866 *
5867 * @param string $message
5868 * @param array $context
5869 *
5870 * @return void
5871 */
5872 public function emergency($message, array $context = array())
5873 {
5874 $this->log(\Kibo\Phast\Logging\LogLevel::EMERGENCY, $message, $context);
5875 }
5876 /**
5877 * Action must be taken immediately.
5878 *
5879 * Example: Entire website down, database unavailable, etc. This should
5880 * trigger the SMS alerts and wake you up.
5881 *
5882 * @param string $message
5883 * @param array $context
5884 *
5885 * @return void
5886 */
5887 public function alert($message, array $context = array())
5888 {
5889 $this->log(\Kibo\Phast\Logging\LogLevel::ALERT, $message, $context);
5890 }
5891 /**
5892 * Critical conditions.
5893 *
5894 * Example: Application component unavailable, unexpected exception.
5895 *
5896 * @param string $message
5897 * @param array $context
5898 *
5899 * @return void
5900 */
5901 public function critical($message, array $context = array())
5902 {
5903 $this->log(\Kibo\Phast\Logging\LogLevel::CRITICAL, $message, $context);
5904 }
5905 /**
5906 * Runtime errors that do not require immediate action but should typically
5907 * be logged and monitored.
5908 *
5909 * @param string $message
5910 * @param array $context
5911 *
5912 * @return void
5913 */
5914 public function error($message, array $context = array())
5915 {
5916 $this->log(\Kibo\Phast\Logging\LogLevel::ERROR, $message, $context);
5917 }
5918 /**
5919 * Exceptional occurrences that are not errors.
5920 *
5921 * Example: Use of deprecated APIs, poor use of an API, undesirable things
5922 * that are not necessarily wrong.
5923 *
5924 * @param string $message
5925 * @param array $context
5926 *
5927 * @return void
5928 */
5929 public function warning($message, array $context = array())
5930 {
5931 $this->log(\Kibo\Phast\Logging\LogLevel::WARNING, $message, $context);
5932 }
5933 /**
5934 * Normal but significant events.
5935 *
5936 * @param string $message
5937 * @param array $context
5938 *
5939 * @return void
5940 */
5941 public function notice($message, array $context = array())
5942 {
5943 $this->log(\Kibo\Phast\Logging\LogLevel::NOTICE, $message, $context);
5944 }
5945 /**
5946 * Interesting events.
5947 *
5948 * Example: User logs in, SQL logs.
5949 *
5950 * @param string $message
5951 * @param array $context
5952 *
5953 * @return void
5954 */
5955 public function info($message, array $context = array())
5956 {
5957 $this->log(\Kibo\Phast\Logging\LogLevel::INFO, $message, $context);
5958 }
5959 /**
5960 * Detailed debug information.
5961 *
5962 * @param string $message
5963 * @param array $context
5964 *
5965 * @return void
5966 */
5967 public function debug($message, array $context = array())
5968 {
5969 $this->log(\Kibo\Phast\Logging\LogLevel::DEBUG, $message, $context);
5970 }
5971 protected function log($level, $message, array $context = array())
5972 {
5973 $context = array_merge(['timestamp' => $this->functions->microtime(true)], $context);
5974 $this->writer->writeEntry(new \Kibo\Phast\Logging\LogEntry($level, $message, array_merge($this->context, $context)));
5975 }
5976 }
5977 namespace Kibo\Phast\Logging;
5978
5979 interface LogReader
5980 {
5981 /**
5982 * Reads LogMessage objects
5983 *
5984 * @return \Generator
5985 */
5986 public function readEntries();
5987 }
5988 namespace Kibo\Phast\Logging;
5989
5990 trait LoggingTrait
5991 {
5992 protected function logger($method = null, $line = null)
5993 {
5994 $context = ['class' => get_class($this)];
5995 if (!is_null($method)) {
5996 $context['method'] = $method;
5997 }
5998 if (!is_null($line)) {
5999 $context['line'] = $line;
6000 }
6001 return \Kibo\Phast\Logging\Log::context($context);
6002 }
6003 }
6004 namespace Kibo\Phast\Logging\LogReaders\JSONLFile;
6005
6006 class Reader implements \Kibo\Phast\Logging\LogReader
6007 {
6008 use \Kibo\Phast\Logging\Common\JSONLFileLogTrait;
6009 public function readEntries()
6010 {
6011 $fp = @fopen($this->filename, 'r');
6012 while ($fp && ($row = @fgets($fp))) {
6013 $decoded = @json_decode($row, true);
6014 if (!$decoded) {
6015 continue;
6016 }
6017 (yield new \Kibo\Phast\Logging\LogEntry(@$decoded['level'], @$decoded['message'], @$decoded['context']));
6018 }
6019 @fclose($fp);
6020 @unlink($this->filename);
6021 }
6022 public function __destruct()
6023 {
6024 if (!($dir = @opendir($this->dir))) {
6025 return;
6026 }
6027 $tenMinutesAgo = time() - 600;
6028 while ($file = @readdir($dir)) {
6029 $filename = $this->dir . "/{$file}";
6030 if (preg_match('/\\.jsonl$/', $file) && @filectime($filename) < $tenMinutesAgo) {
6031 @unlink($filename);
6032 }
6033 }
6034 }
6035 }
6036 namespace Kibo\Phast\Logging;
6037
6038 interface LogWriter
6039 {
6040 /**
6041 * Set a bit-mask to filter entries that are actually written
6042 *
6043 * @param int $mask
6044 * @return void
6045 */
6046 public function setLevelMask($mask);
6047 /**
6048 * Write an entry to the log
6049 *
6050 * @param LogEntry $entry
6051 * @return void
6052 */
6053 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry);
6054 }
6055 namespace Kibo\Phast\Services;
6056
6057 interface ServiceFilter
6058 {
6059 /**
6060 * @param Resource $resource
6061 * @param array $request
6062 * @return Resource
6063 */
6064 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request);
6065 }
6066 namespace Kibo\Phast\Services;
6067
6068 class Factory
6069 {
6070 /**
6071 * @param string $service
6072 * @param array $config
6073 * @return BaseService
6074 * @throws ItemNotFoundException
6075 */
6076 public function make($service, array $config)
6077 {
6078 if (!preg_match('/^[a-z]+$/', $service)) {
6079 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('Bad service');
6080 }
6081 $class = __NAMESPACE__ . '\\' . ucfirst($service) . '\\Factory';
6082 if (class_exists($class)) {
6083 return (new $class())->make($config);
6084 }
6085 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('Unknown service');
6086 }
6087 }
6088 namespace Kibo\Phast\Services;
6089
6090 trait ServiceFactoryTrait
6091 {
6092 /**
6093 * @param array $config
6094 * @param $cacheNamespace
6095 * @return UniversalRetriever
6096 */
6097 public function makeUniversalCachingRetriever(array $config, $cacheNamespace)
6098 {
6099 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
6100 $retriever->addRetriever(new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']));
6101 $retriever->addRetriever(new \Kibo\Phast\Retrievers\CachingRetriever(new \Kibo\Phast\Cache\File\Cache($config['cache'], $cacheNamespace), (new \Kibo\Phast\Retrievers\RemoteRetrieverFactory())->make($config)));
6102 return $retriever;
6103 }
6104 public function makeCachingServiceFilter(array $config, \Kibo\Phast\Filters\Service\CompositeFilter $compositeFilter, $cacheNamespace)
6105 {
6106 return new \Kibo\Phast\Filters\Service\CachingServiceFilter(new \Kibo\Phast\Cache\File\Cache($config['cache'], $cacheNamespace), $compositeFilter, new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']));
6107 }
6108 }
6109 namespace Kibo\Phast\Services\Bundler;
6110
6111 class Service
6112 {
6113 use \Kibo\Phast\Logging\LoggingTrait;
6114 /**
6115 * @var ServiceSignature
6116 */
6117 private $signature;
6118 /**
6119 * @var Retriever
6120 */
6121 private $cssRetriever;
6122 /**
6123 * @var ServiceFilter
6124 */
6125 private $cssFilter;
6126 /**
6127 * @var Retriever
6128 */
6129 private $jsRetriever;
6130 /**
6131 * @var ServiceFilter
6132 */
6133 private $jsFilter;
6134 private $tokenRefMaker;
6135 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)
6136 {
6137 $this->signature = $signature;
6138 $this->cssRetriever = $cssRetriever;
6139 $this->cssFilter = $cssFilter;
6140 $this->jsRetriever = $jsRetriever;
6141 $this->jsFilter = $jsFilter;
6142 $this->tokenRefMaker = $tokenRefMaker;
6143 }
6144 /**
6145 * @param ServiceRequest $request
6146 * @return Response
6147 */
6148 public function serve(\Kibo\Phast\Services\ServiceRequest $request)
6149 {
6150 $response = new \Kibo\Phast\HTTP\Response();
6151 $response->setHeader('Content-Type', 'application/json');
6152 $response->setContent($this->streamResponse($request));
6153 return $response;
6154 }
6155 private function streamResponse(\Kibo\Phast\Services\ServiceRequest $request)
6156 {
6157 (yield '[');
6158 $firstRow = true;
6159 foreach ($this->getParams($request) as $key => $params) {
6160 if (isset($params['ref'])) {
6161 $ref = $params['ref'];
6162 $params = $this->tokenRefMaker->getParams($ref);
6163 if (!$params) {
6164 $this->logger()->error('Could not resolve ref {ref}', ['ref' => $ref]);
6165 (yield $this->generateJSONRow(['status' => 404], $firstRow));
6166 continue;
6167 }
6168 }
6169 if (!isset($params['src'])) {
6170 $this->logger()->error('No src found for set {key}', ['key' => $key]);
6171 (yield $this->generateJSONRow(['status' => 404], $firstRow));
6172 continue;
6173 }
6174 if (!$this->verifyParams($params)) {
6175 $this->logger()->error('Params verification failed for set {key}', ['key' => $key]);
6176 (yield $this->generateJSONRow(['status' => 401], $firstRow));
6177 continue;
6178 }
6179 list($retriever, $filter) = $this->getRetrieverAndFilter($params);
6180 $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever(\Kibo\Phast\ValueObjects\URL::fromString($params['src']), $retriever);
6181 try {
6182 $this->logger()->info('Applying for set {key}', ['key' => $key]);
6183 $filtered = $filter->apply($resource, $params);
6184 (yield $this->generateJSONRow(['status' => 200, 'content' => $filtered->getContent()], $firstRow));
6185 } catch (\Kibo\Phast\Exceptions\ItemNotFoundException $e) {
6186 $this->logger()->error('Could not find {url} for set {key}', ['url' => $params['src'], 'key' => $key]);
6187 (yield $this->generateJSONRow(['status' => 404], $firstRow));
6188 } catch (\Exception $e) {
6189 $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()]);
6190 (yield $this->generateJSONRow(['status' => 500], $firstRow));
6191 }
6192 }
6193 (yield ']');
6194 }
6195 private function getParams(\Kibo\Phast\Services\ServiceRequest $request)
6196 {
6197 $params = $request->getParams();
6198 if (isset($params['src_0'])) {
6199 return (new \Kibo\Phast\Services\Bundler\BundlerParamsParser())->parse($request);
6200 }
6201 return (new \Kibo\Phast\Services\Bundler\ShortBundlerParamsParser())->parse($request);
6202 }
6203 private function verifyParams(array $params)
6204 {
6205 return \Kibo\Phast\Services\Bundler\ServiceParams::fromArray($params)->verify($this->signature);
6206 }
6207 private function getRetrieverAndFilter(array $params)
6208 {
6209 if (isset($params['isScript'])) {
6210 return [$this->jsRetriever, $this->jsFilter];
6211 }
6212 return [$this->cssRetriever, $this->cssFilter];
6213 }
6214 private function generateJSONRow(array $content, &$firstRow)
6215 {
6216 if (!$firstRow) {
6217 $prepend = ',';
6218 } else {
6219 $prepend = '';
6220 $firstRow = false;
6221 }
6222 return $prepend . \Kibo\Phast\Common\JSON::encode($content);
6223 }
6224 }
6225 namespace Kibo\Phast\Services\Bundler;
6226
6227 class BundlerParamsParser
6228 {
6229 public function parse(\Kibo\Phast\Services\ServiceRequest $request)
6230 {
6231 $result = [];
6232 foreach ($request->getParams() as $name => $value) {
6233 if (strpos($name, '_') !== false) {
6234 list($name, $key) = explode('_', $name, 2);
6235 $result[$key][$name] = $value;
6236 }
6237 }
6238 return $result;
6239 }
6240 }
6241 namespace Kibo\Phast\Services\Bundler;
6242
6243 class ShortBundlerParamsParser
6244 {
6245 public static function getParamsMappings()
6246 {
6247 return ['s' => 'src', 'i' => 'strip-imports', 'c' => 'cacheMarker', 't' => 'token', 'j' => 'isScript', 'r' => 'ref'];
6248 }
6249 public function parse(\Kibo\Phast\Services\ServiceRequest $request)
6250 {
6251 $query_string = $request->getHTTPRequest()->getQueryString();
6252 if (preg_match('/(^|&)f=/', $query_string)) {
6253 $query = \Kibo\Phast\ValueObjects\Query::fromString($this->unobfuscateQuery($query_string));
6254 } else {
6255 $query = $request->getQuery();
6256 }
6257 $query = $this->unshortenParams($query->getIterator());
6258 $query = $this->uncompressSrcs($query);
6259 $result = [];
6260 $current = null;
6261 foreach ($query as $key => $value) {
6262 if (in_array($key, ['src', 'ref'])) {
6263 if ($current) {
6264 $result[] = $current;
6265 }
6266 $current = [];
6267 }
6268 if ($current !== null) {
6269 $current[$key] = $value;
6270 }
6271 }
6272 if ($current) {
6273 $result[] = $current;
6274 }
6275 return $result;
6276 }
6277 private function unobfuscateQuery($query)
6278 {
6279 $query = str_rot13($query);
6280 if (strpos($query, '%2S') !== false) {
6281 $query = preg_replace_callback('/%../', function ($match) {
6282 return str_rot13($match[0]);
6283 }, $query);
6284 }
6285 return $query;
6286 }
6287 private function unshortenParams(\Generator $query)
6288 {
6289 $mappings = self::getParamsMappings();
6290 foreach ($query as $key => $value) {
6291 if (isset($mappings[$key])) {
6292 (yield $mappings[$key] => $value === '' ? '1' : $value);
6293 } else {
6294 (yield $key => $value);
6295 }
6296 }
6297 }
6298 private function uncompressSrcs(\Generator $query)
6299 {
6300 $lastUrl = '';
6301 foreach ($query as $key => $value) {
6302 if ($key === 'src') {
6303 $prefixLength = (int) base_convert(substr($value, 0, 2), 36, 10);
6304 $suffix = substr($value, 2);
6305 $value = substr($lastUrl, 0, $prefixLength) . $suffix;
6306 $lastUrl = $value;
6307 }
6308 (yield $key => $value);
6309 }
6310 }
6311 }
6312 namespace Kibo\Phast\Services\Bundler;
6313
6314 class TokenRefMaker
6315 {
6316 private $cache;
6317 public function __construct(\Kibo\Phast\Cache\Cache $cache)
6318 {
6319 $this->cache = $cache;
6320 }
6321 public function getRef($token, array $params)
6322 {
6323 $ref = \Kibo\Phast\Common\Base64url::shortHash(\Kibo\Phast\Common\JSON::encode($params));
6324 $cachedParams = $this->cache->get($ref);
6325 if (!$cachedParams) {
6326 $this->cache->set($ref, $params);
6327 $cachedParams = $this->cache->get($ref);
6328 }
6329 if ($cachedParams === $params) {
6330 return $ref;
6331 }
6332 }
6333 public function getParams($ref)
6334 {
6335 return $this->cache->get($ref);
6336 }
6337 }
6338 namespace Kibo\Phast\Services\Bundler;
6339
6340 class TokenRefMakerFactory
6341 {
6342 public function make(array $config)
6343 {
6344 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'token-refs');
6345 return new \Kibo\Phast\Services\Bundler\TokenRefMaker($cache);
6346 }
6347 }
6348 namespace Kibo\Phast\Services\Bundler;
6349
6350 class ServiceParams
6351 {
6352 /**
6353 * @var string
6354 */
6355 private $token;
6356 /**
6357 * @var array
6358 */
6359 private $params;
6360 private function __construct()
6361 {
6362 }
6363 /**
6364 * @param array $params
6365 * @return ServiceParams
6366 */
6367 public static function fromArray(array $params)
6368 {
6369 $instance = new self();
6370 if (isset($params['token'])) {
6371 $instance->token = $params['token'];
6372 unset($params['token']);
6373 }
6374 $instance->params = $params;
6375 return $instance;
6376 }
6377 /**
6378 * @param ServiceSignature $signature
6379 * @return ServiceParams
6380 */
6381 public function sign(\Kibo\Phast\Security\ServiceSignature $signature)
6382 {
6383 $new = new self();
6384 $new->token = $this->makeToken($signature);
6385 $new->params = $this->params;
6386 return $new;
6387 }
6388 /**
6389 * @param ServiceSignature $signature
6390 * @return bool
6391 */
6392 public function verify(\Kibo\Phast\Security\ServiceSignature $signature)
6393 {
6394 if (!isset($this->token)) {
6395 return false;
6396 }
6397 return $this->token == $this->makeToken($signature);
6398 }
6399 /**
6400 * @return mixed
6401 */
6402 public function toArray()
6403 {
6404 $params = $this->params;
6405 if ($this->token) {
6406 $params['token'] = $this->token;
6407 }
6408 return $params;
6409 }
6410 public function serialize()
6411 {
6412 return \Kibo\Phast\Common\JSON::encode($this->toArray());
6413 }
6414 private function makeToken(\Kibo\Phast\Security\ServiceSignature $signature)
6415 {
6416 $params = $this->params;
6417 if (isset($params['cacheMarker'])) {
6418 unset($params['cacheMarker']);
6419 }
6420 ksort($params);
6421 array_walk($params, function (&$item) {
6422 $item = (string) $item;
6423 });
6424 return $signature->sign(json_encode($params));
6425 }
6426 public function replaceByTokenRef(\Kibo\Phast\Services\Bundler\TokenRefMaker $maker)
6427 {
6428 if (!isset($this->token)) {
6429 return $this;
6430 }
6431 $ref = $maker->getRef($this->token, $this->toArray());
6432 return $ref ? \Kibo\Phast\Services\Bundler\ServiceParams::fromArray(['ref' => $ref]) : $this;
6433 }
6434 }
6435 namespace Kibo\Phast\Services\Bundler;
6436
6437 class Factory
6438 {
6439 use \Kibo\Phast\Services\ServiceFactoryTrait;
6440 public function make(array $config)
6441 {
6442 $cssServiceFactory = new \Kibo\Phast\Services\Css\Factory();
6443 $jsServiceFactory = new \Kibo\Phast\Services\Scripts\Factory();
6444 $cssFilter = $this->makeCachingServiceFilter($config, $cssServiceFactory->makeFilter($config), 'bundler-css');
6445 $jsFilter = $this->makeCachingServiceFilter($config, $jsServiceFactory->makeFilter($config), 'bundler-js');
6446 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));
6447 }
6448 }
6449 namespace Kibo\Phast\Services\Css;
6450
6451 class Factory
6452 {
6453 use \Kibo\Phast\Services\ServiceFactoryTrait;
6454 public function make(array $config)
6455 {
6456 $cssComposite = $this->makeFilter($config);
6457 $composite = $this->makeCachingServiceFilter($config, $cssComposite, 'css-processing-2');
6458 return new \Kibo\Phast\Services\Css\Service((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), [], $this->makeRetriever($config), $composite, $config);
6459 }
6460 public function makeRetriever(array $config)
6461 {
6462 return $this->makeUniversalCachingRetriever($config, 'css');
6463 }
6464 public function makeFilter(array $config)
6465 {
6466 return (new \Kibo\Phast\Filters\CSS\Composite\Factory())->make($config);
6467 }
6468 }
6469 namespace Kibo\Phast\Services\Diagnostics;
6470
6471 class Factory
6472 {
6473 public function make(array $config)
6474 {
6475 $logRoot = null;
6476 foreach ($config['logging']['logWriters'] as $writerConfig) {
6477 if ($writerConfig['class'] == \Kibo\Phast\Logging\LogWriters\JSONLFile\Writer::class) {
6478 $logRoot = $writerConfig['logRoot'];
6479 break;
6480 }
6481 }
6482 return new \Kibo\Phast\Services\Diagnostics\Service($logRoot);
6483 }
6484 }
6485 namespace Kibo\Phast\Services\Diagnostics;
6486
6487 class Service
6488 {
6489 private $logRoot;
6490 public function __construct($logRoot)
6491 {
6492 $this->logRoot = $logRoot;
6493 }
6494 public function serve(\Kibo\Phast\Services\ServiceRequest $request)
6495 {
6496 $params = $request->getParams();
6497 if (isset($params['documentRequestId'])) {
6498 $items = $this->getRequestLog($params['documentRequestId']);
6499 } else {
6500 $items = $this->getSystemDiagnostics();
6501 }
6502 $response = new \Kibo\Phast\HTTP\Response();
6503 $response->setContent(\Kibo\Phast\Common\JSON::prettyEncode($items));
6504 $response->setHeader('Content-Type', 'application/json');
6505 return $response;
6506 }
6507 private function getRequestLog($requestId)
6508 {
6509 return iterator_to_array((new \Kibo\Phast\Logging\LogReaders\JSONLFile\Reader($this->logRoot, $requestId))->readEntries());
6510 }
6511 private function getSystemDiagnostics()
6512 {
6513 return (new \Kibo\Phast\Diagnostics\SystemDiagnostics())->run(require PHAST_CONFIG_FILE);
6514 }
6515 }
6516 namespace Kibo\Phast\Services\Scripts;
6517
6518 class Factory
6519 {
6520 use \Kibo\Phast\Services\ServiceFactoryTrait;
6521 public function make(array $config)
6522 {
6523 $cachedComposite = $this->makeFilter($config);
6524 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);
6525 }
6526 public function makeRetriever(array $config)
6527 {
6528 return $this->makeUniversalCachingRetriever($config, 'scripts');
6529 }
6530 public function makeFilter(array $config)
6531 {
6532 $filter = new \Kibo\Phast\Filters\Service\CompositeFilter();
6533 $filter->addFilter(new \Kibo\Phast\Filters\Text\Decode\Filter());
6534 $filter->addFilter(new \Kibo\Phast\Filters\JavaScript\Minification\JSMinifierFilter(@$config['scripts']['removeLicenseHeaders']));
6535 return $filter;
6536 }
6537 }
6538 namespace Kibo\Phast\Services\Images;
6539
6540 class Factory
6541 {
6542 public function make(array $config)
6543 {
6544 if ($config['images']['api-mode']) {
6545 $retriever = new \Kibo\Phast\Retrievers\PostDataRetriever();
6546 } else {
6547 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
6548 $retriever->addRetriever(new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']));
6549 $retriever->addRetriever((new \Kibo\Phast\Retrievers\RemoteRetrieverFactory())->make($config));
6550 }
6551 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);
6552 }
6553 }
6554 namespace Kibo\Phast\Services;
6555
6556 abstract class BaseService
6557 {
6558 use \Kibo\Phast\Logging\LoggingTrait;
6559 /**
6560 * @var ServiceSignature
6561 */
6562 protected $signature;
6563 /**
6564 * @var string[]
6565 */
6566 protected $whitelist = array();
6567 /**
6568 * @var Retriever
6569 */
6570 protected $retriever;
6571 /**
6572 * @var ServiceFilter
6573 */
6574 protected $filter;
6575 /**
6576 * @var array
6577 */
6578 protected $config;
6579 /**
6580 * BaseService constructor.
6581 * @param ServiceSignature $signature
6582 * @param array $whitelist
6583 * @param Retriever $retriever
6584 * @param ServiceFilter $filter
6585 * @param array $config
6586 */
6587 public function __construct(\Kibo\Phast\Security\ServiceSignature $signature, array $whitelist, \Kibo\Phast\Retrievers\Retriever $retriever, \Kibo\Phast\Services\ServiceFilter $filter, array $config)
6588 {
6589 $this->signature = $signature;
6590 $this->whitelist = $whitelist;
6591 $this->retriever = $retriever;
6592 $this->filter = $filter;
6593 $this->config = $config;
6594 }
6595 /**
6596 * @param ServiceRequest $request
6597 * @return Response
6598 */
6599 public function serve(\Kibo\Phast\Services\ServiceRequest $request)
6600 {
6601 $this->validateRequest($request);
6602 $request = $this->getParams($request);
6603 $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever(\Kibo\Phast\ValueObjects\URL::fromString(isset($request['src']) ? $request['src'] : ''), $this->retriever);
6604 $filtered = $this->filter->apply($resource, $request);
6605 return $this->makeResponse($filtered, $request);
6606 }
6607 /**
6608 * @param ServiceRequest $request
6609 * @return array
6610 */
6611 protected function getParams(\Kibo\Phast\Services\ServiceRequest $request)
6612 {
6613 return $request->getParams();
6614 }
6615 /**
6616 * @param Resource $resource
6617 * @param array $request
6618 * @return Response
6619 */
6620 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
6621 {
6622 $response = new \Kibo\Phast\HTTP\Response();
6623 $response->setContent($resource->getContent());
6624 return $response;
6625 }
6626 protected function validateRequest(\Kibo\Phast\Services\ServiceRequest $request)
6627 {
6628 $this->validateIntegrity($request);
6629 try {
6630 $this->validateToken($request);
6631 } catch (\Kibo\Phast\Exceptions\UnauthorizedException $e) {
6632 $this->validateWhitelisted($request);
6633 }
6634 }
6635 protected function validateIntegrity(\Kibo\Phast\Services\ServiceRequest $request)
6636 {
6637 $params = $request->getParams();
6638 if (!isset($params['src'])) {
6639 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('No source is set!');
6640 }
6641 }
6642 protected function validateToken(\Kibo\Phast\Services\ServiceRequest $request)
6643 {
6644 if (!$request->verify($this->signature)) {
6645 throw new \Kibo\Phast\Exceptions\UnauthorizedException('Invalid token in request: ' . $request->serialize(\Kibo\Phast\Services\ServiceRequest::FORMAT_QUERY));
6646 }
6647 }
6648 protected function validateWhitelisted(\Kibo\Phast\Services\ServiceRequest $request)
6649 {
6650 $params = $request->getParams();
6651 foreach ($this->whitelist as $pattern) {
6652 if (preg_match($pattern, $params['src'])) {
6653 return;
6654 }
6655 }
6656 throw new \Kibo\Phast\Exceptions\UnauthorizedException('Not allowed url: ' . $params['src']);
6657 }
6658 }
6659 namespace Kibo\Phast\Services;
6660
6661 class ServiceRequest
6662 {
6663 const FORMAT_QUERY = 1;
6664 const FORMAT_PATH = 2;
6665 private static $defaultSerializationMode = self::FORMAT_PATH;
6666 /**
6667 * @var string
6668 */
6669 private static $propagatedSwitches = '';
6670 /**
6671 * @var Switches
6672 */
6673 private static $switches;
6674 /**
6675 * @var string
6676 */
6677 private static $documentRequestId;
6678 /**
6679 * @var Request
6680 */
6681 private $httpRequest;
6682 /**
6683 * @var URL
6684 */
6685 private $url;
6686 /**
6687 * @var Query
6688 */
6689 private $query;
6690 /**
6691 * @var string
6692 */
6693 private $token;
6694 /**
6695 * @var bool
6696 */
6697 private $trusted = false;
6698 public function __construct()
6699 {
6700 if (!isset(self::$switches)) {
6701 self::$switches = new \Kibo\Phast\Environment\Switches();
6702 }
6703 $this->query = new \Kibo\Phast\ValueObjects\Query();
6704 }
6705 public static function resetRequestState()
6706 {
6707 self::$defaultSerializationMode = self::FORMAT_PATH;
6708 self::$propagatedSwitches = '';
6709 self::$switches = null;
6710 self::$documentRequestId = null;
6711 }
6712 public static function setDefaultSerializationMode($mode)
6713 {
6714 self::$defaultSerializationMode = $mode;
6715 }
6716 public static function getDefaultSerializationMode()
6717 {
6718 return self::$defaultSerializationMode;
6719 }
6720 public static function fromHTTPRequest(\Kibo\Phast\HTTP\Request $request)
6721 {
6722 $instance = new self();
6723 self::$switches = new \Kibo\Phast\Environment\Switches();
6724 $instance->httpRequest = $request;
6725 if ($request->getCookie('phast')) {
6726 self::$switches = \Kibo\Phast\Environment\Switches::fromString($request->getCookie('phast'));
6727 }
6728 if ($service = self::getRewrittenService($request)) {
6729 $instance->query = \Kibo\Phast\ValueObjects\Query::fromAssoc(['service' => $service, 'src' => $request->getAbsoluteURI()]);
6730 $instance->trusted = true;
6731 } else {
6732 $query = $request->getQuery();
6733 if ($query->get('src')) {
6734 $query->set('src', preg_replace('~^hxxp(?=s?://)~', 'http', $query->get('src')));
6735 }
6736 $pathInfo = $request->getPathInfo();
6737 if ($pathParams = self::parseBase64PathInfo($pathInfo)) {
6738 $query->update($pathParams);
6739 } elseif ($pathInfo) {
6740 $query->update(self::parsePathInfo($pathInfo));
6741 }
6742 if ($token = $query->pop('token')) {
6743 $instance->token = $token;
6744 }
6745 $instance->query = $query;
6746 if ($query->get('phast')) {
6747 self::$propagatedSwitches = $query->get('phast');
6748 $paramsSwitches = \Kibo\Phast\Environment\Switches::fromString($query->get('phast'));
6749 self::$switches = self::$switches->merge($paramsSwitches);
6750 }
6751 if ($query->get('documentRequestId')) {
6752 self::$documentRequestId = $query->get('documentRequestId');
6753 } else {
6754 self::$documentRequestId = (string) mt_rand(0, 999999999);
6755 }
6756 }
6757 return $instance;
6758 }
6759 public static function getRewrittenService(\Kibo\Phast\HTTP\Request $request)
6760 {
6761 if ($service = $request->getEnvValue('REDIRECT_PHAST_SERVICE')) {
6762 return $service;
6763 }
6764 if ($service = $request->getEnvValue('PHAST_SERVICE')) {
6765 return $service;
6766 }
6767 return null;
6768 }
6769 public function hasRequestSwitchesSet()
6770 {
6771 return !empty(self::$propagatedSwitches);
6772 }
6773 /**
6774 * @return Switches
6775 */
6776 public function getSwitches()
6777 {
6778 return self::$switches;
6779 }
6780 /**
6781 * @return array
6782 */
6783 public function getParams()
6784 {
6785 return $this->query->toAssoc();
6786 }
6787 /**
6788 * @return Query
6789 */
6790 public function getQuery()
6791 {
6792 return $this->query;
6793 }
6794 /**
6795 * @return Request
6796 */
6797 public function getHTTPRequest()
6798 {
6799 return $this->httpRequest;
6800 }
6801 /**
6802 * @return string
6803 */
6804 public function getDocumentRequestId()
6805 {
6806 return self::$documentRequestId;
6807 }
6808 /**
6809 * @param array $params
6810 * @return ServiceRequest
6811 */
6812 public function withParams(array $params)
6813 {
6814 $result = clone $this;
6815 $result->query = \Kibo\Phast\ValueObjects\Query::fromAssoc($params);
6816 return $result;
6817 }
6818 /**
6819 * @param URL $url
6820 * @return ServiceRequest
6821 */
6822 public function withUrl(\Kibo\Phast\ValueObjects\URL $url)
6823 {
6824 $result = clone $this;
6825 $result->url = $url;
6826 return $result;
6827 }
6828 /**
6829 * @param ServiceSignature $signature
6830 * @return ServiceRequest
6831 */
6832 public function sign(\Kibo\Phast\Security\ServiceSignature $signature)
6833 {
6834 $token = $signature->sign($this->getVerificationString());
6835 $result = clone $this;
6836 $result->token = $token;
6837 return $result;
6838 }
6839 /**
6840 * @param ServiceSignature $signature
6841 * @return bool
6842 */
6843 public function verify(\Kibo\Phast\Security\ServiceSignature $signature)
6844 {
6845 return $this->trusted || $signature->verify($this->token, $this->getVerificationString()) || $signature->verify($this->token, $this->getVerificationStringWithoutStemSuffix());
6846 }
6847 private static function parsePathInfo($string)
6848 {
6849 $values = new \Kibo\Phast\ValueObjects\Query();
6850 $parts = explode('/', $string);
6851 foreach ($parts as $part) {
6852 if ($part === '') {
6853 continue;
6854 }
6855 $pair = explode('=', $part);
6856 if (isset($pair[1])) {
6857 $values->set($pair[0], self::decodeSingleValue($pair[1]));
6858 } elseif (preg_match('/^__p__(@[1-9][0-9]*x)?\\./', $pair[0], $match)) {
6859 if (!empty($match[1]) && $values->has('src')) {
6860 $values->set('src', self::appendStemSuffix($values->get('src'), $match[1]));
6861 }
6862 break;
6863 } else {
6864 $values->set('src', self::decodeSingleValue($pair[0]));
6865 }
6866 }
6867 return $values;
6868 }
6869 private static function decodeSingleValue($value)
6870 {
6871 return urldecode(str_replace('-', '%', $value));
6872 }
6873 private static function appendStemSuffix($src, $suffix)
6874 {
6875 $url = \Kibo\Phast\ValueObjects\URL::fromString($src);
6876 $path = preg_replace_callback('/\\.\\w+$/', function ($match) use($suffix) {
6877 return $suffix . $match[0];
6878 }, $url->getPath());
6879 return $url->withPath($path)->toString();
6880 }
6881 private static function parseBase64PathInfo($string)
6882 {
6883 if (!preg_match('~^((/[a-z0-9_-]+)+)\\.q\\.js$~i', $string, $match)) {
6884 return null;
6885 }
6886 $data = str_replace('/', '', $match[1]);
6887 return \Kibo\Phast\ValueObjects\Query::fromString(\Kibo\Phast\Common\Base64url::decode($data));
6888 }
6889 /**
6890 * @param callable $paramsFilter
6891 * @return string
6892 */
6893 private function getVerificationString($paramsFilter = null)
6894 {
6895 $params = $this->getAllParams();
6896 if ($paramsFilter) {
6897 $params = $paramsFilter($params);
6898 }
6899 ksort($params);
6900 return http_build_query($params);
6901 }
6902 private function getVerificationStringWithoutStemSuffix()
6903 {
6904 return $this->getVerificationString(function ($params) {
6905 if (isset($params['src'])) {
6906 $params['src'] = $this->stripStemSuffix($params['src']);
6907 }
6908 return $params;
6909 });
6910 }
6911 private function stripStemSuffix($src)
6912 {
6913 $url = \Kibo\Phast\ValueObjects\URL::fromString($src);
6914 $path = preg_replace('/@[1-9][0-9]*x(?=\\.\\w+$)/', '', $url->getPath());
6915 return $url->withPath($path)->toString();
6916 }
6917 public function serialize($format = null)
6918 {
6919 $params = $this->getAllParams();
6920 if ($this->token) {
6921 $params['token'] = $this->token;
6922 }
6923 if (is_null($format)) {
6924 $format = self::$defaultSerializationMode;
6925 }
6926 if ($format == self::FORMAT_PATH) {
6927 return $this->serializeToPathFormat($params);
6928 }
6929 return $this->serializeToQueryFormat($params);
6930 }
6931 private function getAllParams()
6932 {
6933 $urlParams = [];
6934 if ($this->url) {
6935 parse_str($this->url->getQuery(), $urlParams);
6936 }
6937 $params = array_merge($urlParams, $this->query->toAssoc());
6938 if (!empty(self::$propagatedSwitches)) {
6939 $params['phast'] = self::$propagatedSwitches;
6940 }
6941 if (self::$switches->isOn(\Kibo\Phast\Environment\Switches::SWITCH_DIAGNOSTICS)) {
6942 $params['documentRequestId'] = self::$documentRequestId;
6943 }
6944 return $params;
6945 }
6946 private function serializeToQueryFormat(array $params)
6947 {
6948 $encoded = http_build_query($params);
6949 if (!isset($this->url)) {
6950 return $encoded;
6951 }
6952 $serialized = preg_replace('~\\?.*~', '', (string) $this->url);
6953 if (self::$defaultSerializationMode === self::FORMAT_PATH && !preg_match('~/$~', $serialized)) {
6954 $serialized .= '/' . $this->getDummyFilename($params);
6955 }
6956 return $serialized . '?' . $encoded;
6957 }
6958 /** @return string */
6959 private function serializeToPathFormat(array $params)
6960 {
6961 $encodedSrc = null;
6962 $values = [];
6963 foreach (explode('&', http_build_query($params)) as $element) {
6964 list($key, $value) = explode('=', $element, 2);
6965 $encodedValue = str_replace(['-', '%'], ['%2D', '-'], $value);
6966 if ($key == 'src') {
6967 $encodedSrc = $encodedValue;
6968 } else {
6969 $values[] = $key . '=' . $encodedValue;
6970 }
6971 }
6972 if ($encodedSrc) {
6973 array_unshift($values, $encodedSrc);
6974 }
6975 $params = '/' . join('/', $values) . '/' . $this->getDummyFilename($params);
6976 if (isset($this->url)) {
6977 return preg_replace(['~\\?.*~', '~/$~'], '', $this->url) . $params;
6978 }
6979 return $params;
6980 }
6981 private function getDummyFilename(array $params)
6982 {
6983 return '__p__.' . $this->getDummyExtension($params);
6984 }
6985 private function getDummyExtension(array $params)
6986 {
6987 $default = 'js';
6988 if (empty($params['src'])) {
6989 return $default;
6990 }
6991 $url = \Kibo\Phast\ValueObjects\URL::fromString($params['src']);
6992 $ext = strtolower($url->getExtension());
6993 if (preg_match('/^(jpe?g|gif|png|js|css)$/', $ext)) {
6994 return $ext;
6995 }
6996 return $default;
6997 }
6998 }
6999 namespace Kibo\Phast\Security;
7000
7001 class ServiceSignature
7002 {
7003 const AUTO_TOKEN_SIZE = 128;
7004 const SIGNATURE_LENGTH = 16;
7005 /**
7006 * @var Cache
7007 */
7008 private $cache;
7009 /**
7010 * @var array
7011 */
7012 private $identities;
7013 /**
7014 * ServiceSignature constructor.
7015 *
7016 * @param Cache $cache
7017 */
7018 public function __construct(\Kibo\Phast\Cache\Cache $cache)
7019 {
7020 $this->cache = $cache;
7021 }
7022 /**
7023 * @param string|array $identities
7024 */
7025 public function setIdentities($identities)
7026 {
7027 if (is_string($identities)) {
7028 $this->identities = ['' => $identities];
7029 } else {
7030 $this->identities = $identities;
7031 }
7032 }
7033 /**
7034 * @return string
7035 */
7036 public function getCacheSalt()
7037 {
7038 $identities = $this->getIdentities();
7039 return md5(join('=>', array_merge(array_keys($identities), array_values($identities))));
7040 }
7041 public function sign($value)
7042 {
7043 $identities = $this->getIdentities();
7044 $users = array_keys($identities);
7045 list($user, $token) = [array_shift($users), array_shift($identities)];
7046 return $user . substr(md5($token . $value), 0, self::SIGNATURE_LENGTH);
7047 }
7048 public function verify($signature, $value)
7049 {
7050 $user = substr($signature, 0, -self::SIGNATURE_LENGTH);
7051 $identities = $this->getIdentities();
7052 if (!isset($identities[$user])) {
7053 return false;
7054 }
7055 $token = $identities[$user];
7056 $signer = new self($this->cache);
7057 $signer->setIdentities([$user => $token]);
7058 return $signature === $signer->sign($value);
7059 }
7060 public static function generateToken()
7061 {
7062 $token = '';
7063 for ($i = 0; $i < self::AUTO_TOKEN_SIZE; $i++) {
7064 $token .= chr(mt_rand(33, 126));
7065 }
7066 return $token;
7067 }
7068 private function getIdentities()
7069 {
7070 if (!isset($this->identities)) {
7071 $token = $this->cache->get('security-token', function () {
7072 return self::generateToken();
7073 });
7074 $this->identities = ['' => $token];
7075 }
7076 return $this->identities;
7077 }
7078 }
7079 namespace Kibo\Phast\Security;
7080
7081 class ServiceSignatureFactory
7082 {
7083 public function make(array $config)
7084 {
7085 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'signature');
7086 $signature = new \Kibo\Phast\Security\ServiceSignature($cache);
7087 if (isset($config['securityToken'])) {
7088 $signature->setIdentities($config['securityToken']);
7089 }
7090 return $signature;
7091 }
7092 }
7093 namespace Kibo\Phast\Exceptions;
7094
7095 class LogicException extends \LogicException
7096 {
7097 }
7098 namespace Kibo\Phast\Exceptions;
7099
7100 class RuntimeException extends \RuntimeException
7101 {
7102 }
7103 namespace Kibo\Phast\Exceptions;
7104
7105 class CachedExceptionException extends \Exception
7106 {
7107 }
7108 namespace Kibo\Phast\Exceptions;
7109
7110 class ItemNotFoundException extends \Exception
7111 {
7112 /**
7113 * @var URL
7114 */
7115 private $url;
7116 public function __construct($message = '', $code = 0, \Throwable $previous = null, \Kibo\Phast\ValueObjects\URL $failed = null)
7117 {
7118 parent::__construct($message, $code, $previous);
7119 $this->url = $failed;
7120 }
7121 /**
7122 * @return URL
7123 */
7124 public function getUrl()
7125 {
7126 return $this->url;
7127 }
7128 }
7129 namespace Kibo\Phast\Exceptions;
7130
7131 class UnauthorizedException extends \Exception
7132 {
7133 }
7134 namespace Kibo\Phast\Exceptions;
7135
7136 class UndefinedObjectifiedFunction extends \RuntimeException
7137 {
7138 }
7139 namespace Kibo\PhastPlugins\SDK;
7140
7141 /**
7142 * Provides commonly needed URLs
7143 *
7144 * Interface HostURLs
7145 * @see URL
7146 */
7147 interface HostURLs
7148 {
7149 /**
7150 * The URL at which static resource (JS, CSS, IMG)
7151 * optimizations reside
7152 *
7153 * @return URL
7154 */
7155 public function getServicesURL();
7156 /**
7157 * The full URL of the root of the current site
7158 *
7159 * @return URL
7160 */
7161 public function getSiteURL();
7162 /**
7163 * The CDN equivalent of a specified URL
7164 *
7165 * @return URL
7166 */
7167 public function getCDNURL(\Kibo\Phast\ValueObjects\URL $url);
7168 /**
7169 * URL of the admin page at which
7170 * the plugin's settings are located
7171 *
7172 * @return URL
7173 */
7174 public function getSettingsURL();
7175 /**
7176 * URL for admin panel AJAX communication
7177 *
7178 * @return URL
7179 */
7180 public function getAJAXEndPoint();
7181 /**
7182 * A URL to a publicly available image.
7183 *
7184 * @return URL
7185 */
7186 public function getTestImageURL();
7187 }
7188 namespace Kibo\PhastPlugins\SDK;
7189
7190 /**
7191 * Services container for the Phast Plugins Services SDK
7192 *
7193 * Class SDK
7194 */
7195 class ServiceSDK
7196 {
7197 /**
7198 * @var ServiceHost
7199 */
7200 protected $host;
7201 /**
7202 * @var EnvironmentIdentifier
7203 */
7204 private $environmentIdentifier;
7205 public function __construct(\Kibo\PhastPlugins\SDK\ServiceHost $host)
7206 {
7207 $this->host = $host;
7208 $this->environmentIdentifier = new \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier();
7209 }
7210 public function getServiceAPI()
7211 {
7212 return new \Kibo\PhastPlugins\SDK\APIs\Service($this->getServiceConfiguration());
7213 }
7214 public function getServiceConfiguration()
7215 {
7216 return new \Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration($this->getPHPFilesServiceConfigurationRepository(), $this->getEnvironmentIdentifier(), $this->getCacheRootManager(), [$this->host, 'onServiceConfigurationLoad']);
7217 }
7218 public function getCacheRootManager()
7219 {
7220 return new \Kibo\PhastPlugins\SDK\Caching\CacheRootManager($this->host->getCacheRootCandidatesProvider());
7221 }
7222 /**
7223 * Returns a default implementation of the
7224 * ServiceConfigurationRepository interface
7225 *
7226 * @see ServiceConfigurationRepository
7227 * @return PHPFilesServiceConfigurationRepository
7228 */
7229 public function getPHPFilesServiceConfigurationRepository()
7230 {
7231 return new \Kibo\PhastPlugins\SDK\Configuration\PHPFilesServiceConfigurationRepository($this->getCacheRootManager());
7232 }
7233 public function getEnvironmentIdentifier()
7234 {
7235 return $this->environmentIdentifier;
7236 }
7237 }
7238 namespace Kibo\PhastPlugins\SDK;
7239
7240 /**
7241 * Services container for the Phast Plugins SDK
7242 *
7243 * Class SDK
7244 */
7245 class SDK extends \Kibo\PhastPlugins\SDK\ServiceSDK
7246 {
7247 /**
7248 * @var PluginHost
7249 */
7250 protected $host;
7251 /**
7252 * SDK constructor.
7253 * @param PluginHost $host
7254 */
7255 public function __construct(\Kibo\PhastPlugins\SDK\PluginHost $host)
7256 {
7257 parent::__construct($host);
7258 }
7259 /**
7260 * The current SDK version
7261 *
7262 * @return string
7263 */
7264 public function getSDKVersion()
7265 {
7266 return '8';
7267 }
7268 /**
7269 * The current plugin version.
7270 * Composed from the host plugin name,
7271 * the host plugin version
7272 * and the SDK version
7273 *
7274 * @return string
7275 */
7276 public function getPluginVersion()
7277 {
7278 return join('-', [$this->host->getPluginHostName(), $this->host->getPluginHostVersion(), $this->getSDKVersion()]);
7279 }
7280 /**
7281 * @return Phast
7282 */
7283 public function getPhastAPI()
7284 {
7285 return new \Kibo\PhastPlugins\SDK\APIs\Phast($this->getPhastConfiguration());
7286 }
7287 public function getAdminPanel()
7288 {
7289 return new \Kibo\PhastPlugins\SDK\AdminPanel\AdminPanel($this->host->getPhastUser(), $this->host->getHostURLs()->getAJAXEndPoint(), $this->getAdminPanelData(), $this->getTranslationsManager(), $this->host->isDev());
7290 }
7291 public function getAJAXRequestsDispatcher()
7292 {
7293 return new \Kibo\PhastPlugins\SDK\AJAX\RequestsDispatcher($this->host->getPhastUser(), $this);
7294 }
7295 public function getAdminPanelData()
7296 {
7297 return new \Kibo\PhastPlugins\SDK\AdminPanel\AdminPanelData($this->getPluginConfiguration(), $this->getServiceConfigurationGenerator(), $this->getPhastConfiguration(), $this->getCacheRootManager(), $this->host);
7298 }
7299 public function getInstallNotice()
7300 {
7301 return new \Kibo\PhastPlugins\SDK\AdminPanel\InstallNotice($this->getPluginConfiguration(), $this->host->getInstallNoticeRenderer(), $this->getTranslationsManager(), $this->host->getHostURLs()->getSettingsURL(), $this->host->getHostURLs()->getAJAXEndPoint());
7302 }
7303 public function getPluginConfiguration()
7304 {
7305 return new \Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration($this->getPluginConfigurationRepository(), $this->getServiceConfigurationGenerator(), $this->getCacheRootManager(), $this->host->getPhastUser(), $this->host->getNonceChecker());
7306 }
7307 public function getPhastConfiguration()
7308 {
7309 return new \Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration($this->getServiceConfigurationGenerator(), $this->getServiceConfiguration(), $this->getPluginConfiguration(), [$this->host, 'onPhastConfigurationLoad']);
7310 }
7311 public function getAutoConfiguration()
7312 {
7313 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());
7314 }
7315 public function getTranslationsManager()
7316 {
7317 return new \Kibo\PhastPlugins\SDK\AdminPanel\TranslationsManager($this->host->getLocale(), $this->host->getPluginName());
7318 }
7319 private function getServiceConfigurationGenerator()
7320 {
7321 return new \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator($this->getPHPFilesServiceConfigurationRepository(), $this->getEnvironmentIdentifier(), $this->getPluginVersion(), $this->host->getHostURLs());
7322 }
7323 public function updatePreviewCookie($enable = true)
7324 {
7325 if (headers_sent()) {
7326 return false;
7327 }
7328 $enabled = isset($_COOKIE['PHAST_PREVIEW']) && (bool) $_COOKIE['PHAST_PREVIEW'];
7329 if (!$enabled && $enable) {
7330 return setcookie('PHAST_PREVIEW', '1', 0, '/');
7331 }
7332 if ($enabled && !$enable) {
7333 return setcookie('PHAST_PREVIEW', '0', 0, '/');
7334 }
7335 return true;
7336 }
7337 private function getPluginConfigurationRepository()
7338 {
7339 return new \Kibo\PhastPlugins\SDK\Configuration\PluginConfigurationRepository($this->host->getKeyValueStore());
7340 }
7341 }
7342 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7343
7344 /**
7345 * Represents the data that needs to be send
7346 * to the plugin's admin panel
7347 *
7348 * Class AdminPanelData
7349 */
7350 class AdminPanelData
7351 {
7352 /**
7353 * @var PluginConfiguration
7354 */
7355 private $pluginConfig;
7356 /**
7357 * @var ServiceConfigurationGenerator
7358 */
7359 private $serviceConfigGenerator;
7360 /**
7361 * @var PhastConfiguration
7362 */
7363 private $phastConfig;
7364 /**
7365 * @var CacheRootManager
7366 */
7367 private $cacheRootManager;
7368 /**
7369 * @var PluginHost
7370 */
7371 private $host;
7372 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)
7373 {
7374 $this->pluginConfig = $pluginConfig;
7375 $this->serviceConfigGenerator = $serviceConfigGenerator;
7376 $this->phastConfig = $phastConfig;
7377 $this->cacheRootManager = $cacheRootManager;
7378 $this->host = $host;
7379 }
7380 public function get()
7381 {
7382 $siteUrl = $this->host->getHostURLs()->getSiteURL();
7383 $urlWithPhast = $this->addQueryParam($siteUrl, 'phast', 'phast');
7384 $urlWithoutPhast = $this->addQueryParam($siteUrl, 'phast', '-phast');
7385 $pageSpeedToolUrl = 'https://developers.google.com/speed/pagespeed/insights/?url=';
7386 $errors = [];
7387 if (!$this->cacheRootManager->hasCacheRoot()) {
7388 $errors[] = ['type' => 'no-cache-root', 'params' => $this->cacheRootManager->getCacheRootCandidates()];
7389 }
7390 if (!$this->serviceConfigGenerator->generateIfNotExists($this->pluginConfig)) {
7391 $errors[] = ['type' => 'no-service-config', 'params' => $this->cacheRootManager->getCacheRootCandidates()];
7392 }
7393 $warnings = [];
7394 $api_client_warning = [];
7395 $phast_config = $this->phastConfig->get();
7396 $diagnostics = new \Kibo\Phast\Diagnostics\SystemDiagnostics();
7397 foreach ($diagnostics->run($phast_config) as $status) {
7398 if ($status->isAvailable()) {
7399 continue;
7400 }
7401 $package = $status->getPackage();
7402 $type = $package->getType();
7403 if ($type == 'Cache') {
7404 $errors[] = ['type' => 'cache', 'params' => [$status->getReason()]];
7405 } elseif ($type == 'ImageFilter') {
7406 $name = substr($package->getNamespace(), strrpos($package->getNamespace(), '\\') + 1);
7407 if ($name === 'ImageAPIClient') {
7408 $api_client_warning[] = 'Image optimization API error: ' . $status->getReason();
7409 } else {
7410 $warnings[] = $status->getReason();
7411 }
7412 }
7413 }
7414 $phastpress_config = $this->pluginConfig->get();
7415 if ($phastpress_config['img-optimization-api']) {
7416 $warnings = $api_client_warning;
7417 }
7418 $nonce = $this->host->getNonce();
7419 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()];
7420 }
7421 private function addQueryParam(\Kibo\Phast\ValueObjects\URL $url, $key, $value)
7422 {
7423 // TODO: Move this functionality to URL class
7424 $urlStr = (string) $url;
7425 $glue = strpos($urlStr, '?') === false ? '?' : '&';
7426 return $urlStr . $glue . $key . '=' . $value;
7427 }
7428 }
7429 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7430
7431 /**
7432 * Represents an installation notice
7433 * displayed everywhere in the host system's admin panel
7434 * upon plugin activation.
7435 *
7436 * Class InstallNotice
7437 */
7438 class InstallNotice
7439 {
7440 /**
7441 * @var PluginConfiguration
7442 */
7443 private $config;
7444 /**
7445 * @var InstallNoticeRenderer
7446 */
7447 private $renderer;
7448 /**
7449 * @var TranslationsManager
7450 */
7451 private $translations;
7452 /**
7453 * @var URL
7454 */
7455 private $settingsUrl;
7456 /**
7457 * @var URL
7458 */
7459 private $ajaxEntryPoint;
7460 /**
7461 * InstallNotice constructor.
7462 * @param PluginConfiguration $config
7463 * @param InstallNoticeRenderer $renderer
7464 * @param TranslationsManager $translations
7465 * @param URL $settingsUrl
7466 * @param URL $ajaxEndPoint
7467 */
7468 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)
7469 {
7470 $this->config = $config;
7471 $this->renderer = $renderer;
7472 $this->translations = $translations;
7473 $this->settingsUrl = $settingsUrl;
7474 $this->ajaxEntryPoint = $ajaxEndPoint;
7475 }
7476 /**
7477 * @return string The HTML to render the notice
7478 */
7479 public function render()
7480 {
7481 $display_message = $this->config->shouldShowActivationNotification();
7482 if (!$display_message) {
7483 return '';
7484 }
7485 $config = $this->config->get();
7486 if ($config['enabled'] && $config['admin-only']) {
7487 $status = 'Backend.status.admin';
7488 } elseif ($config['enabled']) {
7489 $status = 'Backend.status.on';
7490 } else {
7491 $status = 'Backend.status.off';
7492 }
7493 $message = $this->translations->get('Backend.install-notice', ['pluginState' => $this->translations->get($status), 'settingsUrl' => (string) $this->settingsUrl]);
7494 $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 ";
7495 return $this->renderer->render($message, $onCloseFunction);
7496 }
7497 }
7498 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7499
7500 interface InstallNoticeRenderer
7501 {
7502 /**
7503 * Renders a system notice.
7504 *
7505 * @param string $notice The message to show in the notice
7506 * @param string $onCloseJSFunction JavaScript function to call on the client when
7507 * an event that closes the notice occurs.
7508 * @return string HTML for the notice
7509 * @see InstallNotice
7510 */
7511 public function render($notice, $onCloseJSFunction);
7512 }
7513 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7514
7515 /**
7516 * Represents a nonce form field used XSS defense
7517 *
7518 * Class Nonce
7519 */
7520 class Nonce implements \JsonSerializable
7521 {
7522 /**
7523 * @var string
7524 */
7525 private $fieldName;
7526 /**
7527 * @var string
7528 */
7529 private $value;
7530 private function __construct()
7531 {
7532 }
7533 /**
7534 * @param string $fieldName The name of the field in the form
7535 * @param string $value The value of the field
7536 * @return Nonce
7537 */
7538 public static function make($fieldName, $value)
7539 {
7540 $instance = new self();
7541 $instance->fieldName = $fieldName;
7542 $instance->value = $value;
7543 return $instance;
7544 }
7545 /**
7546 * @return string
7547 */
7548 public function getFieldName()
7549 {
7550 return $this->fieldName;
7551 }
7552 /**
7553 * @return string
7554 */
7555 public function getValue()
7556 {
7557 return $this->value;
7558 }
7559 public function jsonSerialize()
7560 {
7561 return ['fieldName' => $this->fieldName, 'value' => $this->value];
7562 }
7563 }
7564 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7565
7566 /**
7567 * Represents the admin panel used for plugin configuration.
7568 * Use this class for rendering the admin panel of the plugin.
7569 *
7570 * Class AdminPanel
7571 */
7572 class AdminPanel
7573 {
7574 /**
7575 * @var PhastUser
7576 */
7577 private $user;
7578 /**
7579 * @var URL
7580 */
7581 private $ajaxEndPoint;
7582 /**
7583 * @var AdminPanelData
7584 */
7585 private $data;
7586 /**
7587 * @var TranslationsManager
7588 */
7589 private $translations;
7590 private $isDev = 'prod';
7591 private $styles = array('prod' => array('app.css'), 'dev' => array());
7592 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'));
7593 /**
7594 * AdminPanel constructor.
7595 * @param PhastUser $user
7596 * @param URL $ajaxEndPoint
7597 * @param AdminPanelData $data
7598 * @param TranslationsManager $translations
7599 * @param bool $isDev
7600 */
7601 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)
7602 {
7603 $this->user = $user;
7604 $this->ajaxEndPoint = $ajaxEndPoint;
7605 $this->data = $data;
7606 $this->translations = $translations;
7607 $this->isDev = (bool) $isDev;
7608 }
7609 /**
7610 * Returns the HTML needed to display the admin panel
7611 *
7612 * @return string
7613 */
7614 public function render()
7615 {
7616 if (!$this->user->mayModifySettings()) {
7617 return '';
7618 }
7619 $id = 'phast-plugins-sdk-admin-panel';
7620 $template = $this->getResourcesString();
7621 $template .= sprintf('
7622 <div id="%1$s"></div>
7623 <script>
7624 try {
7625 window.PHAST_PLUGINS_SDK_ADMIN_PANEL.apply(window, %2$s)
7626 } catch (e) {
7627 document.getElementById("%1$s").innerText = "Error: " + e.message
7628 throw e
7629 }
7630 </script>
7631 ', $id, json_encode([$id, $this->ajaxEndPoint->toString(), $this->data->get(), $this->translations->getAll()]));
7632 return $template;
7633 }
7634 private function getResourcesString()
7635 {
7636 return $this->isDev ? $this->getDevResourcesString() : $this->getProdResources();
7637 }
7638 private function getProdResources()
7639 {
7640 $resources = '';
7641 $base = __DIR__ . '/static/';
7642 $cssBase = $base . 'css/';
7643 foreach ($this->styles['prod'] as $style) {
7644 $resources .= '<style>' . file_get_contents($cssBase . $style) . '</style>';
7645 }
7646 $jsBase = $base . 'js/';
7647 foreach ($this->scripts['prod'] as $script) {
7648 $resources .= '<script>' . file_get_contents($jsBase . $script) . '</script>';
7649 }
7650 return $resources;
7651 }
7652 private function getDevResourcesString()
7653 {
7654 $resources = '';
7655 foreach ($this->styles['dev'] as $href) {
7656 $resources .= "<link rel=\"stylesheet\" href=\"{$href}\">";
7657 }
7658 foreach ($this->scripts['dev'] as $src) {
7659 $resources .= "<script src=\"{$src}\"></script>";
7660 }
7661 return $resources;
7662 }
7663 }
7664 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7665
7666 class TranslationsManager
7667 {
7668 const DEFAULT_LOCALE = 'en';
7669 /**
7670 * @var string
7671 */
7672 private $locale;
7673 /**
7674 * @var string
7675 */
7676 private $pluginName;
7677 /**
7678 * @var string
7679 */
7680 private $languagesDir;
7681 /**
7682 * @var array
7683 */
7684 private $modules = array();
7685 public function __construct($locale, $pluginName)
7686 {
7687 $data = \Kibo\PhastPlugins\SDK\Generated\Translations::DATA;
7688 if (isset($data[$this->locale])) {
7689 $this->locale = $locale;
7690 } else {
7691 $this->locale = self::DEFAULT_LOCALE;
7692 }
7693 $this->pluginName = $pluginName;
7694 }
7695 public function getAll()
7696 {
7697 return array_merge(['plugin-name' => $this->pluginName], \Kibo\PhastPlugins\SDK\Generated\Translations::DATA[$this->locale]);
7698 }
7699 public function get($key, $interpolationArguments = array())
7700 {
7701 $keyParts = explode('.', $key);
7702 $transArr = \Kibo\PhastPlugins\SDK\Generated\Translations::DATA[$this->locale];
7703 while (count($keyParts) > 0) {
7704 $part = array_shift($keyParts);
7705 if (!isset($transArr[$part])) {
7706 return $key;
7707 }
7708 $transArr = $transArr[$part];
7709 }
7710 if (is_string($transArr)) {
7711 return $this->interpolate($transArr, $interpolationArguments);
7712 }
7713 return $key;
7714 }
7715 private function interpolate($string, $arguments)
7716 {
7717 $keys = array_map(function ($str) {
7718 return '{' . $str . '}';
7719 }, array_keys($arguments));
7720 $params = array_combine($keys, array_values($arguments));
7721 $params['@:plugin-name'] = $this->pluginName;
7722 return strtr($string, $params);
7723 }
7724 }
7725 namespace Kibo\PhastPlugins\SDK\Configuration;
7726
7727 /**
7728 * Represents the javascript used
7729 * for auto-configuration done
7730 * immediately after activation of the plugin.
7731 *
7732 * Class AutoConfiguration
7733 */
7734 class AutoConfiguration
7735 {
7736 /**
7737 * @var PluginConfiguration
7738 */
7739 private $pluginConfig;
7740 /**
7741 * @var PhastConfiguration
7742 */
7743 private $phastConfig;
7744 /**
7745 * @var URL
7746 */
7747 private $servicesUrl;
7748 /**
7749 * @var URL
7750 */
7751 private $testImageUrl;
7752 /**
7753 * @var Nonce
7754 */
7755 private $nonce;
7756 /**
7757 * @var URL
7758 */
7759 private $ajaxEndPoint;
7760 /**
7761 * AutoConfiguration constructor.
7762 * @param PluginConfiguration $pluginConfig
7763 * @param PhastConfiguration $phastConfig
7764 * @param URL $servicesUrl
7765 * @param URL $testImageUrl
7766 * @param Nonce $nonce
7767 * @param URL $ajaxEndPoint
7768 */
7769 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)
7770 {
7771 $this->pluginConfig = $pluginConfig;
7772 $this->phastConfig = $phastConfig;
7773 $this->servicesUrl = $servicesUrl;
7774 $this->testImageUrl = $testImageUrl;
7775 $this->nonce = $nonce;
7776 $this->ajaxEndPoint = $ajaxEndPoint;
7777 }
7778 /**
7779 * Returns the script that needs to rendered
7780 * in order for the script to get executed.
7781 *
7782 * @return string
7783 */
7784 public function renderScript()
7785 {
7786 if (!$this->pluginConfig->shouldAutoConfigure()) {
7787 return '';
7788 }
7789 $config = \Kibo\Phast\Environment\Configuration::fromDefaults()->withUserConfiguration(new \Kibo\Phast\Environment\Configuration($this->phastConfig->get()))->getRuntimeConfig()->toArray();
7790 $signature = (new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config);
7791 $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);
7792 $nonce = json_encode($this->nonce);
7793 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>";
7794 }
7795 }
7796 namespace Kibo\PhastPlugins\SDK\Configuration;
7797
7798 class ServiceConfigurationGenerator
7799 {
7800 /**
7801 * @var ServiceConfigurationRepository
7802 */
7803 private $repository;
7804 /**
7805 * @var EnvironmentIdentifier
7806 */
7807 private $environmentIdentifier;
7808 /**
7809 * @var URL
7810 */
7811 private $servicesUrl;
7812 /**
7813 * @var URL
7814 */
7815 private $cdnServicesUrl;
7816 /**
7817 * @var string
7818 */
7819 private $pluginVersion;
7820 /**
7821 * @var string
7822 */
7823 private $cdnHost;
7824 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository $repository, \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier $environmentIdentifier, $pluginVersion, \Kibo\PhastPlugins\SDK\HostURLs $hostUrls)
7825 {
7826 $this->repository = $repository;
7827 $this->environmentIdentifier = $environmentIdentifier;
7828 $this->pluginVersion = $pluginVersion;
7829 $this->servicesUrl = $hostUrls->getServicesURL();
7830 $this->cdnServicesUrl = $hostUrls->getCDNURL($hostUrls->getServicesURL());
7831 $this->cdnHost = $hostUrls->getCDNURL($hostUrls->getSiteURL())->getHost();
7832 }
7833 public function generateIfNotExists(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig)
7834 {
7835 if (!$this->repository->has()) {
7836 return $this->generate($pluginConfig);
7837 }
7838 $config = $this->repository->get();
7839 $envId = $this->environmentIdentifier->getValue();
7840 if (empty($config['plugin_version']) || $config['plugin_version'] != $this->pluginVersion || empty($config['alternativeServicesUrls'][$envId]) || $config['alternativeServicesUrls'][$envId] != $this->getServicesURLString($pluginConfig)) {
7841 return $this->generate($pluginConfig);
7842 }
7843 return true;
7844 }
7845 public function generate(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig)
7846 {
7847 $previousConfig = $this->repository->get();
7848 $plugin_config = $pluginConfig->get();
7849 $plugin_version = $this->pluginVersion;
7850 $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];
7851 if (isset($previousConfig['alternativeServicesUrls'])) {
7852 $config['alternativeServicesUrls'] = $previousConfig['alternativeServicesUrls'];
7853 } else {
7854 $config['alternativeServicesUrls'] = [];
7855 }
7856 $id = $this->environmentIdentifier->getValue();
7857 unset($config['alternativeServicesUrls'][$id]);
7858 $config['alternativeServicesUrls'][$id] = $this->getServicesURLString($pluginConfig);
7859 $config['alternativeServicesUrls'] = array_slice($config['alternativeServicesUrls'], -1000);
7860 return $this->repository->store($config);
7861 }
7862 private function getServicesURLString(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig)
7863 {
7864 $plugin_config = $pluginConfig->get();
7865 if ($plugin_config['pathinfo-query-format']) {
7866 return (string) $this->cdnServicesUrl;
7867 }
7868 return (string) $this->servicesUrl;
7869 }
7870 }
7871 namespace Kibo\PhastPlugins\SDK\Configuration;
7872
7873 /**
7874 * Represents the configuration
7875 * that needs to be passed to be returned
7876 * by the callback passed to
7877 * \Kibo\Phast\PhastServices::serve()
7878 *
7879 * @see \Kibo\Phast\PhastServices::serve()
7880 * Class ServiceConfiguration
7881 */
7882 class ServiceConfiguration
7883 {
7884 /**
7885 * @var ServiceConfigurationRepository
7886 */
7887 private $repository;
7888 /**
7889 * @var EnvironmentIdentifier
7890 */
7891 private $environmentIdentifier;
7892 /**
7893 * @var CacheRootManager
7894 */
7895 private $cacheRootManager;
7896 /**
7897 * @var callable
7898 */
7899 private $onLoadCb;
7900 /**
7901 * ServiceConfiguration constructor.
7902 * @param ServiceConfigurationRepository $repository
7903 * @param CacheRootManager $cacheRootManager
7904 * @param callable $onLoadCb
7905 */
7906 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository $repository, \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier $environmentIdentifier, \Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager, callable $onLoadCb)
7907 {
7908 $this->repository = $repository;
7909 $this->environmentIdentifier = $environmentIdentifier;
7910 $this->cacheRootManager = $cacheRootManager;
7911 $this->onLoadCb = $onLoadCb;
7912 }
7913 /**
7914 * Returns the configuration as config
7915 *
7916 * @return array|bool|mixed
7917 */
7918 public function get()
7919 {
7920 $config = $this->repository->get();
7921 $envId = $this->environmentIdentifier->getValue();
7922 if (isset($config['alternativeServicesUrls'][$envId])) {
7923 $config['servicesUrl'] = $config['alternativeServicesUrls'][$envId];
7924 }
7925 if (!empty($config['cdnHost'])) {
7926 $config['retrieverMap'][$config['cdnHost']] = \Kibo\Phast\HTTP\Request::fromGlobals()->getDocumentRoot();
7927 }
7928 $config['cache'] = ['cacheRoot' => $this->cacheRootManager->getCacheRoot()];
7929 $apiFilterName = \Kibo\Phast\Filters\Image\ImageAPIClient\Filter::class;
7930 $api_enabled = $config['images']['filters'][$apiFilterName]['enabled'];
7931 if (!$api_enabled) {
7932 unset($config['images']['filters'][$apiFilterName]);
7933 return call_user_func($this->onLoadCb, $config);
7934 }
7935 $config['images']['filters'][$apiFilterName]['host-name'] = $_SERVER['HTTP_HOST'];
7936 $config['images']['filters'][$apiFilterName]['request-uri'] = $_SERVER['REQUEST_URI'];
7937 $config['images']['filters'][$apiFilterName]['api-url'] = 'https://optimize.phast.io/?service=images';
7938 return call_user_func($this->onLoadCb, $config);
7939 }
7940 }
7941 namespace Kibo\PhastPlugins\SDK\Configuration;
7942
7943 class EnvironmentIdentifier
7944 {
7945 private $value;
7946 public function __construct()
7947 {
7948 $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']}");
7949 }
7950 public function getValue()
7951 {
7952 return $this->value;
7953 }
7954 }
7955 namespace Kibo\PhastPlugins\SDK\Configuration;
7956
7957 /**
7958 * Represents the configuration
7959 * that needs to be passed to
7960 * \Kibo\Phast\PhastDocumentFilters::deploy()
7961 * and
7962 * \Kibo\Phast\PhastDocumentFilters::apply()
7963 *
7964 * @see \Kibo\Phast\PhastDocumentFilters
7965 * Class PhastConfiguration
7966 */
7967 class PhastConfiguration
7968 {
7969 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));
7970 /**
7971 * @var ServiceConfigurationGenerator
7972 */
7973 private $serviceConfigGenerator;
7974 /**
7975 * @var ServiceConfiguration
7976 */
7977 private $serviceConfig;
7978 /**
7979 * @var PluginConfiguration
7980 */
7981 private $pluginConfig;
7982 /**
7983 * @var callable
7984 */
7985 private $onLoadCb;
7986 /**
7987 * PhastConfiguration constructor.
7988 * @param ServiceConfigurationGenerator $serviceConfigGenerator
7989 * @param ServiceConfiguration $serviceConfig
7990 * @param PluginConfiguration $pluginConfig
7991 * @param callable $onLoadCb
7992 */
7993 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator $serviceConfigGenerator, \Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration $serviceConfig, \Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig, callable $onLoadCb)
7994 {
7995 $this->serviceConfigGenerator = $serviceConfigGenerator;
7996 $this->serviceConfig = $serviceConfig;
7997 $this->pluginConfig = $pluginConfig;
7998 $this->onLoadCb = $onLoadCb;
7999 }
8000 /**
8001 * Returns the configuration to use on full html documents as an array
8002 *
8003 * @return array|bool|mixed
8004 */
8005 public function getForDocuments()
8006 {
8007 list($phastConfig, $pluginConfig) = $this->getPhastAndPluginConfigs();
8008 foreach (array_keys(self::SETTINGS_2_FILTERS) as $setting) {
8009 $this->setSettingInPhastConfig($setting, $pluginConfig, $phastConfig);
8010 }
8011 return call_user_func($this->onLoadCb, $phastConfig);
8012 }
8013 public function getForHTMLSnippets()
8014 {
8015 list($phastConfig, $pluginConfig) = $this->getPhastAndPluginConfigs();
8016 $defaultConfig = \Kibo\Phast\Environment\Configuration::fromDefaults()->toArray();
8017 $allFilters = array_keys($defaultConfig['documents']['filters']);
8018 foreach ($allFilters as $filter) {
8019 $phastConfig['documents']['filters'][$filter]['enabled'] = false;
8020 }
8021 $this->setSettingInPhastConfig('img-optimization-tags', $pluginConfig, $phastConfig);
8022 $this->setSettingInPhastConfig('img-optimization-css', $pluginConfig, $phastConfig);
8023 $this->setSettingInPhastConfig('img-lazy', $pluginConfig, $phastConfig);
8024 $phastConfig['optimizeHTMLDocumentsOnly'] = false;
8025 $phastConfig['outputServerSideStats'] = false;
8026 return call_user_func($this->onLoadCb, $phastConfig);
8027 }
8028 private function getPhastAndPluginConfigs()
8029 {
8030 // TODO: Optimize so we do not read from the service config file a bunch of times
8031 $this->serviceConfigGenerator->generateIfNotExists($this->pluginConfig);
8032 $pluginConfig = $this->pluginConfig->get();
8033 $phastConfig = $this->serviceConfig->get();
8034 $phastConfig['documents']['filters'] = [];
8035 $phastConfig['switches']['phast'] = $phastConfig && $this->pluginConfig->shouldDeployFilters();
8036 return [$phastConfig, $pluginConfig];
8037 }
8038 private function setSettingInPhastConfig($settingName, $pluginConfig, &$phastConfig)
8039 {
8040 foreach (self::SETTINGS_2_FILTERS[$settingName] as $filterClass) {
8041 if (!class_exists($filterClass)) {
8042 throw new \LogicException("No such filter: {$filterClass}");
8043 }
8044 if (strpos($filterClass, \Kibo\Phast\Filters\HTML::class . '\\') === 0) {
8045 $object = 'documents';
8046 } elseif (strpos($filterClass, \Kibo\Phast\Filters\CSS::class . '\\') === 0) {
8047 $object = 'styles';
8048 } else {
8049 throw new \LogicException("Invalid filter namespace: {$filterClass}");
8050 }
8051 $phastConfig[$object]['filters'][$filterClass] = ['enabled' => $settingName];
8052 $phastConfig['switches'][$settingName] = $pluginConfig[$settingName];
8053 }
8054 }
8055 /**
8056 * Returns the configuration as an array
8057 *
8058 * @return array|bool|mixed
8059 * @deprecated use PhastConfiguration::getForDocuments()
8060 */
8061 public function get()
8062 {
8063 return $this->getForDocuments();
8064 }
8065 }
8066 namespace Kibo\PhastPlugins\SDK\Configuration;
8067
8068 /**
8069 * Manages serialization of the phast service configuration.
8070 * Must be as fast as possible, having as little
8071 * dependency on the host system as possible (ideally - none).
8072 *
8073 * Interface ServiceConfigurationRepository
8074 */
8075 interface ServiceConfigurationRepository
8076 {
8077 /**
8078 * Store the given config
8079 *
8080 * @param array $config
8081 * @return bool TRUE on success, FALSE on failure
8082 */
8083 public function store(array $config);
8084 /**
8085 * Returns the previously stored config
8086 *
8087 * @return array|bool - The config on success or
8088 * FALSE on failure or if no config has been stored
8089 */
8090 public function get();
8091 /**
8092 * Tells whether a config has been previously stored
8093 *
8094 * @return bool
8095 */
8096 public function has();
8097 }
8098 namespace Kibo\PhastPlugins\SDK\Configuration;
8099
8100 class PluginConfigurationRepository
8101 {
8102 /**
8103 * @var KeyValueStore
8104 */
8105 private $store;
8106 /**
8107 * JSONKeyValueStore constructor.
8108 * @param KeyValueStore $store
8109 */
8110 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\KeyValueStore $store)
8111 {
8112 $this->store = $store;
8113 }
8114 /**
8115 * @param mixed $key
8116 * @param null $default
8117 * @return mixed|null
8118 */
8119 public function get($key, $default = null)
8120 {
8121 $value = $this->store->get($key);
8122 if (!is_string($value) || $value === 'null') {
8123 return $default;
8124 }
8125 $deserialised = @json_decode($value, true);
8126 if (is_null($deserialised)) {
8127 return $default;
8128 }
8129 return $deserialised;
8130 }
8131 /**
8132 * @param mixed $key
8133 * @param mixed $value
8134 */
8135 public function set($key, $value)
8136 {
8137 $this->store->set($key, json_encode($value));
8138 }
8139 }
8140 namespace Kibo\PhastPlugins\SDK\Configuration;
8141
8142 /**
8143 * A key-value store for use within the plugin's admin panel
8144 *
8145 * Interface KeyValueStore
8146 */
8147 interface KeyValueStore
8148 {
8149 /**
8150 * @param string $key
8151 * @return string|null The previously stored value or
8152 * null if there was no value stored for this key
8153 */
8154 public function get($key);
8155 /**
8156 * @param string $key
8157 * @param string $value
8158 * @return void
8159 */
8160 public function set($key, $value);
8161 }
8162 namespace Kibo\PhastPlugins\SDK\Configuration;
8163
8164 /**
8165 * Represents the configuration of the plugin
8166 *
8167 * Class PluginConfiguration
8168 */
8169 class PluginConfiguration
8170 {
8171 const KEY_SETTINGS = 'settings';
8172 const KEY_ACTIVATION_NOTIFICATION = 'activation-notification';
8173 /**
8174 * @var PluginConfigurationRepository
8175 */
8176 private $repo;
8177 /**
8178 * @var ServiceConfigurationGenerator
8179 */
8180 private $serviceConfigGenerator;
8181 /**
8182 * @var CacheRootManager
8183 */
8184 private $cacheRootManager;
8185 /**
8186 * @var PhastUser
8187 */
8188 private $user;
8189 /**
8190 * @var NonceChecker
8191 */
8192 private $nonceChecker;
8193 /**
8194 * PluginConfiguration constructor.
8195 * @param PluginConfigurationRepository $repo
8196 * @param ServiceConfigurationGenerator $serviceConfigGenerator
8197 * @param CacheRootManager $cacheRootManager
8198 * @param PhastUser $user
8199 * @param NonceChecker $nonceChecker
8200 */
8201 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)
8202 {
8203 $this->repo = $repo;
8204 $this->serviceConfigGenerator = $serviceConfigGenerator;
8205 $this->cacheRootManager = $cacheRootManager;
8206 $this->user = $user;
8207 $this->nonceChecker = $nonceChecker;
8208 }
8209 public function get()
8210 {
8211 $userSettings = $this->repo->get(self::KEY_SETTINGS, []);
8212 return array_merge($this->getDefaultAdminPanelSettings(), $userSettings);
8213 }
8214 public function save(array $newConfig)
8215 {
8216 if (!$this->nonceChecker->checkNonce($newConfig)) {
8217 return;
8218 }
8219 $keys = array_keys($this->getDefaultAdminPanelSettings());
8220 $settings = [];
8221 foreach ($keys as $key) {
8222 $newConfigKey = "phastpress-{$key}";
8223 if (!isset($newConfig[$newConfigKey])) {
8224 continue;
8225 }
8226 if ($newConfig[$newConfigKey] == 'on') {
8227 $settings[$key] = true;
8228 } elseif ($newConfig[$newConfigKey] == 'off') {
8229 $settings[$key] = false;
8230 }
8231 }
8232 $this->update($settings);
8233 }
8234 public function update(array $settings)
8235 {
8236 $this->repo->set(self::KEY_SETTINGS, array_merge($this->get(), $settings));
8237 $this->serviceConfigGenerator->generate($this);
8238 }
8239 public function shouldShowActivationNotification()
8240 {
8241 return $this->repo->get(self::KEY_ACTIVATION_NOTIFICATION, true);
8242 }
8243 public function hideActivationNotification()
8244 {
8245 $this->repo->set(self::KEY_ACTIVATION_NOTIFICATION, false);
8246 }
8247 public function shouldAutoConfigure()
8248 {
8249 return !$this->repo->get(self::KEY_SETTINGS);
8250 }
8251 public function shouldDeployFilters()
8252 {
8253 $plugin_config = $this->get();
8254 if (!$plugin_config['enabled']) {
8255 return false;
8256 }
8257 if (!$plugin_config['admin-only']) {
8258 return true;
8259 }
8260 return $this->user->seesPreviewMode();
8261 }
8262 public function shouldDisplayFooter()
8263 {
8264 return $this->get()['footer-link'] && $this->shouldDeployFilters();
8265 }
8266 private function getDefaultAdminPanelSettings()
8267 {
8268 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];
8269 }
8270 }
8271 namespace Kibo\PhastPlugins\SDK\Configuration;
8272
8273 /**
8274 * A default implementation of the ServiceConfigurationRepository interface
8275 *
8276 * Class PHPFilesServiceConfigurationRepository
8277 */
8278 class PHPFilesServiceConfigurationRepository implements \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository
8279 {
8280 /**
8281 * @var CacheRootManager
8282 */
8283 private $cacheRootManager;
8284 /**
8285 * PHPFilesServiceConfigurationRepository constructor.
8286 * @param CacheRootManager $cacheRootManager
8287 */
8288 public function __construct(\Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager)
8289 {
8290 $this->cacheRootManager = $cacheRootManager;
8291 }
8292 public function store(array $config)
8293 {
8294 return $this->storeInPHPFile($this->getServiceConfigurationFilePath(), $config) !== false;
8295 }
8296 public function get()
8297 {
8298 $json = $this->readFromPHPFile($this->getServiceConfigurationFilePath());
8299 if (!$json) {
8300 return false;
8301 }
8302 if (strpos($json, 'a:') === 0) {
8303 $config = unserialize($json);
8304 } else {
8305 $config = json_decode($json, true);
8306 }
8307 if ($config === null) {
8308 return false;
8309 }
8310 return $config;
8311 }
8312 public function has()
8313 {
8314 return !!$this->get();
8315 }
8316 private function getServiceConfigurationFilePath()
8317 {
8318 return $this->getCacheStoredFilePath('service-config');
8319 }
8320 private function getCacheStoredFilePath($filename)
8321 {
8322 $dir = $this->cacheRootManager->getCacheRoot();
8323 if (!$dir) {
8324 return false;
8325 }
8326 $legacyName = "{$dir}/{$filename}.php";
8327 if (@file_exists($legacyName)) {
8328 return $legacyName;
8329 }
8330 foreach (@scandir($dir) as $file) {
8331 if (!preg_match('~^' . preg_quote($filename, '~') . '-[a-zA-Z0-9]{16}$~', $file)) {
8332 continue;
8333 }
8334 $path = "{$dir}/{$file}";
8335 if (@is_file($path)) {
8336 return $path;
8337 }
8338 }
8339 return "{$dir}/service-config-{$this->generateRandomName()}";
8340 }
8341 private function generateRandomName()
8342 {
8343 $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8344 $o = '';
8345 for ($i = 0; $i < 16; $i++) {
8346 $o .= $chars[mt_rand(0, strlen($chars) - 1)];
8347 }
8348 return $o;
8349 }
8350 private function readFromPHPFile($filename)
8351 {
8352 $content = @file_get_contents($filename);
8353 if (!$content) {
8354 return false;
8355 }
8356 if (!preg_match('/^[^>]*>\\n([a-f0-9]{40})\\n(.*)$/s', $content, $match)) {
8357 return false;
8358 }
8359 if (sha1($match[2]) != $match[1]) {
8360 return false;
8361 }
8362 return $match[2];
8363 }
8364 private function storeInPHPFile($filename, $value)
8365 {
8366 $value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES) . "\n";
8367 $content = "<?php exit; ?>\n" . sha1($value) . "\n" . $value;
8368 return @file_put_contents($filename, $content, LOCK_EX);
8369 }
8370 }
8371 namespace Kibo\PhastPlugins\SDK\APIs;
8372
8373 /**
8374 * Presents convenient methods for common tasks.
8375 *
8376 * Class Service
8377 */
8378 class Service
8379 {
8380 /**
8381 * @var ServiceConfiguration
8382 */
8383 private $config;
8384 /**
8385 * Service constructor.
8386 * @param ServiceConfiguration $config
8387 */
8388 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration $config)
8389 {
8390 $this->config = $config;
8391 }
8392 /**
8393 * Configures the services and serves the request
8394 */
8395 public function serve()
8396 {
8397 \Kibo\Phast\PhastServices::serve(function () {
8398 return $this->config->get();
8399 });
8400 }
8401 }
8402 namespace Kibo\PhastPlugins\SDK\APIs;
8403
8404 /**
8405 * Presents convenient methods for common tasks.
8406 *
8407 * Class Phast
8408 */
8409 class Phast
8410 {
8411 /**
8412 * @var PhastConfiguration
8413 */
8414 private $config;
8415 /**
8416 * PhastAPI constructor.
8417 * @param PhastConfiguration $config
8418 */
8419 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration $config)
8420 {
8421 $this->config = $config;
8422 }
8423 /**
8424 * Applies phast filters to $html
8425 * with a configuration suited for full documents
8426 *
8427 * @param $html
8428 * @return string
8429 */
8430 public function applyFiltersForDocument($html)
8431 {
8432 return \Kibo\Phast\PhastDocumentFilters::apply($html, $this->config->getForDocuments());
8433 }
8434 /**
8435 * Applies phast filters to $html
8436 * with a configuration suited for html snippets
8437 *
8438 * @param $html
8439 * @return string
8440 */
8441 public function applyFiltersForSnippets($html)
8442 {
8443 return \Kibo\Phast\PhastDocumentFilters::apply($html, $this->config->getForHTMLSnippets());
8444 }
8445 /**
8446 * Deploys phast output buffer filters
8447 * with a configuration suited for full documents
8448 */
8449 public function deployOutputBufferForDocument()
8450 {
8451 return \Kibo\Phast\PhastDocumentFilters::deploy($this->config->getForDocuments());
8452 }
8453 /**
8454 * Deploys phast output buffer filters
8455 * with a configuration suited for html snippets
8456 */
8457 public function deployOutputBufferForSnippets()
8458 {
8459 return \Kibo\Phast\PhastDocumentFilters::deploy($this->config->getForHTMLSnippets());
8460 }
8461 }
8462 namespace Kibo\PhastPlugins\SDK\Caching;
8463
8464 interface CacheRootCandidatesProvider
8465 {
8466 /**
8467 * Return a list of folders that will potentially be used
8468 * for storing cache and service configuration files.
8469 * The directories will be checked for write access
8470 * in the order they were provided. The first one writable
8471 * will be used.
8472 *
8473 * @return string[]
8474 */
8475 public function getCacheRootCandidates();
8476 }
8477 namespace Kibo\PhastPlugins\SDK\Caching;
8478
8479 class CacheRootManager
8480 {
8481 /**
8482 * @var CacheRootCandidatesProvider
8483 */
8484 private $rootsProvider;
8485 /**
8486 * CacheRootManager constructor.
8487 * @param CacheRootCandidatesProvider $rootsProvider
8488 */
8489 public function __construct(\Kibo\PhastPlugins\SDK\Caching\CacheRootCandidatesProvider $rootsProvider)
8490 {
8491 $this->rootsProvider = $rootsProvider;
8492 }
8493 public function getCacheRootCandidates()
8494 {
8495 return $this->rootsProvider->getCacheRootCandidates();
8496 }
8497 public function getCacheRoot()
8498 {
8499 $key = $this->getKey();
8500 $candidates = $this->getCacheRootCandidates();
8501 if ($result = $this->findExistingCacheRoot($key, $candidates)) {
8502 return $result;
8503 }
8504 if ($this->createNewCacheRoot($key, $candidates)) {
8505 return $this->findExistingCacheRoot($key, $candidates);
8506 }
8507 return false;
8508 }
8509 public function hasCacheRoot()
8510 {
8511 return (bool) $this->getCacheRoot();
8512 }
8513 public function getAllCacheRoots()
8514 {
8515 return $this->findAllExistingCacheRoots($this->getKey(), $this->getCacheRootCandidates());
8516 }
8517 private function getKey()
8518 {
8519 return md5(@$_SERVER['DOCUMENT_ROOT']) . '.' . (new \Kibo\Phast\Common\System())->getUserId();
8520 }
8521 private function findExistingCacheRoot($key, $candidates)
8522 {
8523 foreach ($this->findAllExistingCacheRoots($key, $candidates) as $checkDir) {
8524 if (!is_writable($checkDir)) {
8525 continue;
8526 }
8527 if (function_exists('posix_geteuid') && fileowner($checkDir) !== posix_geteuid()) {
8528 continue;
8529 }
8530 $this->createIndexFile($checkDir);
8531 return $checkDir;
8532 }
8533 return false;
8534 }
8535 private function findAllExistingCacheRoots($key, $candidates)
8536 {
8537 foreach ($this->getCacheRootCandidates() as $dir) {
8538 $checkDirs = ["{$dir}/{$key}", "{$dir}/phastpress.{$key}", "{$dir}/phast.{$key}"];
8539 foreach ($checkDirs as $checkDir) {
8540 if (!is_dir($checkDir)) {
8541 continue;
8542 }
8543 (yield $checkDir);
8544 }
8545 }
8546 }
8547 private function createNewCacheRoot($key, $candidates)
8548 {
8549 foreach ($this->getCacheRootCandidates() as $dir) {
8550 if (@mkdir("{$dir}/phast.{$key}", 0777, true)) {
8551 return true;
8552 }
8553 }
8554 return false;
8555 }
8556 private function createIndexFile($dir)
8557 {
8558 $path = "{$dir}/index.html";
8559 if (!@file_exists($path)) {
8560 @touch($path);
8561 }
8562 }
8563 }
8564 namespace Kibo\PhastPlugins\SDK;
8565
8566 class Autoloader
8567 {
8568 private static $instance;
8569 private $psr4 = array();
8570 public static function getInstance()
8571 {
8572 if (!isset(self::$instance)) {
8573 self::$instance = new self();
8574 self::$instance->install();
8575 }
8576 return self::$instance;
8577 }
8578 public function install()
8579 {
8580 spl_autoload_register(function ($class) {
8581 $this->autoload($class);
8582 });
8583 }
8584 public function addPSR4($namespace, $dir)
8585 {
8586 $this->psr4[] = [$namespace, $dir];
8587 return $this;
8588 }
8589 private function autoload($class)
8590 {
8591 foreach ($this->psr4 as $psr4) {
8592 list($namespace, $dir) = $psr4;
8593 if (strcasecmp($namespace . '\\', substr($class, 0, strlen($namespace) + 1))) {
8594 continue;
8595 }
8596 $relativeName = substr($class, strlen($namespace) + 1);
8597 $relativePath = str_replace('\\', '/', $relativeName) . '.php';
8598 $fullPath = $dir . '/' . $relativePath;
8599 if (file_exists($fullPath)) {
8600 include $fullPath;
8601 return;
8602 }
8603 }
8604 }
8605 }
8606 namespace Kibo\PhastPlugins\SDK;
8607
8608 interface ServiceHost
8609 {
8610 /**
8611 * @return CacheRootCandidatesProvider
8612 */
8613 public function getCacheRootCandidatesProvider();
8614 /**
8615 * Called right after the service configuration
8616 * has been loaded. Use it to modify the config
8617 * and take any other needed action before
8618 * the service is started.
8619 *
8620 * @param array $config - The configuration that has been loaded
8621 * @return array - The configuration to use for the services
8622 */
8623 public function onServiceConfigurationLoad(array $config);
8624 }
8625 namespace Kibo\PhastPlugins\SDK\Generated;
8626
8627 class Translations
8628 {
8629 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!
8630 ')), '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>.
8631 ', '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 set long cache durations.', 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.'))))));
8632 }
8633 namespace Kibo\PhastPlugins\SDK\AJAX;
8634
8635 /**
8636 * Handles AJAX requests to the plugin admin.
8637 * Use this class to handle requests to the plugin's admin AJAX end point
8638 *
8639 * Class RequestsDispatcher
8640 */
8641 class RequestsDispatcher
8642 {
8643 const KEY_ACTION = 'phast-plugins-action';
8644 /**
8645 * @var PhastUser
8646 */
8647 private $user;
8648 /**
8649 * @var SDK
8650 */
8651 private $sdk;
8652 /**
8653 * RequestsDispatcher constructor.
8654 * @param PhastUser $user
8655 * @param SDK $sdk
8656 */
8657 public function __construct(\Kibo\PhastPlugins\SDK\Security\PhastUser $user, \Kibo\PhastPlugins\SDK\SDK $sdk)
8658 {
8659 $this->user = $user;
8660 $this->sdk = $sdk;
8661 }
8662 /**
8663 * Handles ajax requests to plugin's admin AJAX end point
8664 *
8665 * @param array $request The $_POST data to the request
8666 * @return mixed Must be json encoded and returned to the client
8667 */
8668 public function dispatch(array $request)
8669 {
8670 if (!$this->user->mayModifySettings()) {
8671 return false;
8672 }
8673 $action = isset($request[self::KEY_ACTION]) ? $request[self::KEY_ACTION] : '';
8674 if ($action == 'save-settings') {
8675 $this->sdk->getPluginConfiguration()->save($request);
8676 return $this->makeResponse(true, $this->sdk->getAdminPanelData()->get());
8677 }
8678 if ($action == 'dismiss-notice') {
8679 $this->sdk->getPluginConfiguration()->hideActivationNotification();
8680 return $this->makeResponse(true);
8681 }
8682 return $this->makeResponse(false);
8683 }
8684 private function makeResponse($success, $data = null)
8685 {
8686 return ['phast-success' => $success, 'phast-data' => $data];
8687 }
8688 }
8689 namespace Kibo\PhastPlugins\SDK\Security;
8690
8691 /**
8692 * Represents the user currently viewing the website (either backend or frontend)
8693 *
8694 * Interface PhastUser
8695 */
8696 interface PhastUser
8697 {
8698 /**
8699 * Tells whether the user can access and manipulate
8700 * the plugin's settings
8701 *
8702 * @return bool
8703 */
8704 public function mayModifySettings();
8705 /**
8706 * Tells whether the user can access the website with Phast enabled in preview mode
8707 *
8708 * @return bool
8709 */
8710 public function seesPreviewMode();
8711 }
8712 namespace Kibo\PhastPlugins\SDK\Security;
8713
8714 /**
8715 * Checks whether posted data to the server
8716 * contains the expected nonce.
8717 *
8718 * Interface NonceChecker
8719 */
8720 interface NonceChecker
8721 {
8722 /**
8723 * Performs the check
8724 *
8725 * @param array $data The data posted to the server
8726 * @return bool TRUE if all is well, FALSE otherwise
8727 */
8728 public function checkNonce(array $data);
8729 }
8730 namespace Kibo\PhastPlugins\SDK\Common;
8731
8732 /**
8733 * Contains common implementations for methods
8734 * of the PluginHost interface
8735 *
8736 * @see PluginHost
8737 * Trait PluginHostTrait
8738 */
8739 trait PluginHostTrait
8740 {
8741 public function getPluginName()
8742 {
8743 return 'Phast';
8744 }
8745 public function isDev()
8746 {
8747 return $this->getPluginHostVersion() === '$VER' . 'SION$';
8748 }
8749 public function onPhastConfigurationLoad(array $config)
8750 {
8751 return $config;
8752 }
8753 public function getLocale()
8754 {
8755 return 'en';
8756 }
8757 public function getInstallNoticeRenderer()
8758 {
8759 return new \Kibo\PhastPlugins\SDK\AdminPanel\DefaultInstallNoticeRenderer();
8760 }
8761 }
8762 namespace Kibo\PhastPlugins\SDK\Common;
8763
8764 trait ServiceHostTrait
8765 {
8766 public function onServiceConfigurationLoad(array $config)
8767 {
8768 return $config;
8769 }
8770 }
8771 namespace Kibo\PhastPlugins\SDK\Common;
8772
8773 trait PreviewCookieTrait
8774 {
8775 public function seesPreviewMode()
8776 {
8777 return isset($_COOKIE['PHAST_PREVIEW']) && (bool) $_COOKIE['PHAST_PREVIEW'];
8778 }
8779 }
8780 namespace Kibo\Phast\Environment\Exceptions;
8781
8782 class PackageHasNoDiagnosticsException extends \Kibo\Phast\Exceptions\LogicException
8783 {
8784 }
8785 namespace Kibo\Phast\Environment\Exceptions;
8786
8787 class PackageHasNoFactoryException extends \Kibo\Phast\Exceptions\LogicException
8788 {
8789 }
8790 namespace Kibo\Phast\Cache\File;
8791
8792 class DiagnosticsLogWriter implements \Kibo\Phast\Logging\LogWriter
8793 {
8794 public function setLevelMask($mask)
8795 {
8796 }
8797 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry)
8798 {
8799 if ($entry->getLevel() > 2) {
8800 $needles = array_map(function ($key) {
8801 return '{' . $key . '}';
8802 }, array_keys($entry->getContext()));
8803 $message = str_replace($needles, $entry->getContext(), $entry->getMessage());
8804 throw new \Kibo\Phast\Exceptions\RuntimeException("Error: Level: {$entry->getLevel()}, Msg: {$message}");
8805 }
8806 }
8807 }
8808 namespace Kibo\Phast\Cache\File;
8809
8810 class Diagnostics implements \Kibo\Phast\Diagnostics\Diagnostics
8811 {
8812 public function diagnose(array $config)
8813 {
8814 \Kibo\Phast\Logging\Log::setLogger(new \Kibo\Phast\Logging\Logger(new \Kibo\Phast\Cache\File\DiagnosticsLogWriter()));
8815 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'cache-self-diagnosis');
8816 $v1 = $cache->get('test-key', function () {
8817 return 1;
8818 }, 2);
8819 $v2 = $cache->get('test-key', function () {
8820 return 2;
8821 }, 2);
8822 if ($v1 != $v2) {
8823 throw new \Kibo\Phast\Exceptions\RuntimeException('Cache failed, but no error was reported!');
8824 }
8825 }
8826 }
8827 namespace Kibo\Phast\Filters\HTML\CommentsRemoval;
8828
8829 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
8830 {
8831 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
8832 {
8833 foreach ($elements as $element) {
8834 if (!$element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Comment || $element->isIEConditional()) {
8835 (yield $element);
8836 }
8837 }
8838 }
8839 }
8840 namespace Kibo\Phast\Filters\HTML\ScriptsDeferring;
8841
8842 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
8843 {
8844 use \Kibo\Phast\Filters\HTML\Helpers\JSDetectorTrait;
8845 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
8846 {
8847 return $tag->getTagName() == 'script';
8848 }
8849 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
8850 {
8851 if ($this->isJSElement($script) && !$this->isDeferralDisabled($script)) {
8852 $this->rewrite($script);
8853 }
8854 (yield $script);
8855 }
8856 protected function afterLoop()
8857 {
8858 $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();var ca;if(isAsync(\$)){ca=\"async\"}else if(isDefer(\$)){ca=\"defer\"}s.write=function(ga){if(ca){console.warn(\"document.write call from \"+ca+\" script ignored\");return}da(ga)};s.writeln=function(ha){if(ca){console.warn(\"document.writeln call from \"+ca+\" script ignored\");return}da(ha+\"\\n\")};function da(ia){var ja=s.createElement(\"div\");ja.innerHTML=ia;var ka=ea(ja);if(aa&&aa.parentNode!==\$.parentNode){aa=\$.nextElementSibling}while(ja.firstChild){\$.parentNode.insertBefore(ja.firstChild,aa)}ka.map(fa)}function ea(la){return Array.prototype.slice.call(la.getElementsByTagName(\"script\")).filter(function(ma){var na=ma.getAttribute(\"type\");return!na||/^(text|application)\\/javascript(;|\$)/i.test(na)})}function fa(oa){var pa=new phast.ScriptsLoader.Scripts.Factory(s);var qa=pa.makeScriptFromElement(oa);ba=phast.ScriptsLoader.chainScript(ba,qa)}return _().then(function(){return ba}).finally(function(){delete s.write;delete s.writeln})}function B(ra){var sa=s.createElement(\"link\");sa.setAttribute(\"rel\",\"preload\");sa.setAttribute(\"as\",\"script\");sa.setAttribute(\"href\",ra);s.head.appendChild(sa)}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(ta,ua){this._utils=ta;this._element=ua;this.init=function(){return Promise.resolve()};this.execute=function(){var va=ua.textContent.replace(/^\\s*<!--.*\\n/i,\"\");ta.restoreOriginals(ua);return ta.writeProtectAndExecuteString(ua,va)};this.describe=function(){return\"inline script\"}};phast.ScriptsLoader.Scripts.AsyncBrowserScript=function(wa,xa){var ya;this._utils=wa;this._element=xa;this.init=function(){wa.addPreload(getSrc(xa));return new Promise(function(za){ya=za})};this.execute=function(){var Aa=wa.copyElement(xa);wa.restoreOriginals(Aa);wa.replaceElement(xa,Aa).then(ya).catch(ya);return Promise.resolve()};this.describe=function(){return\"async script at \"+getSrc(xa)}};phast.ScriptsLoader.Scripts.SyncBrowserScript=function(Ba,Ca){this._utils=Ba;this._element=Ca;this.init=function(){Ba.addPreload(getSrc(Ca));return Promise.resolve()};this.execute=function(){var Da=Ba.copyElement(Ca);Ba.restoreOriginals(Da);return Ba.writeProtectAndReplaceElement(Ca,Da)};this.describe=function(){return\"sync script at \"+getSrc(Ca)}};phast.ScriptsLoader.Scripts.AsyncAJAXScript=function(Ea,Fa,Ga,Ha){this._utils=Ea;this._element=Fa;this._fetch=Ga;this._fallback=Ha;var Ia;var Ja;this.init=function(){Ia=Ga(Fa);return new Promise(function(Ka){Ja=Ka})};this.execute=function(){Ia.then(function(La){Ea.restoreOriginals(Fa);return Ea.writeProtectAndExecuteString(Fa,La).then(Ja)}).catch(function(){Ha.init();return Ha.execute().then(Ja)});return Promise.resolve()};this.describe=function(){return\"bundled async script at \"+Fa.getAttribute(\"data-phast-original-src\")}};phast.ScriptsLoader.Scripts.SyncAJAXScript=function(Ma,Na,Oa,Pa){this._utils=Ma;this._element=Na;this._fetch=Oa;this._fallback=Pa;var Qa;this.init=function(){Qa=Oa(Na);return Qa};this.execute=function(){return Qa.then(function(Ra){Ma.restoreOriginals(Na);return Ma.writeProtectAndExecuteString(Na,Ra)}).catch(function(){Pa.init();return Pa.execute()})};this.describe=function(){return\"bundled sync script at \"+Na.getAttribute(\"data-phast-original-src\")}};phast.ScriptsLoader.Scripts.Factory=function(Sa,Ta){var Ua=phast.ScriptsLoader.Scripts;var Va=new phast.ScriptsLoader.Utilities(Sa);this.makeScriptFromElement=function(Ya){var Za;if(Wa(Ya)){if(isAsync(Ya)){Za=new Ua.AsyncBrowserScript(Va,Ya);return Ta?new Ua.AsyncAJAXScript(Va,Ya,Ta,Za):Za}Za=new Ua.SyncBrowserScript(Va,Ya);return Ta?new Ua.SyncAJAXScript(Va,Ya,Ta,Za):Za}if(Xa(Ya)){return new Ua.InlineScript(Va,Ya)}if(isAsync(Ya)){return new Ua.AsyncBrowserScript(Va,Ya)}return new Ua.SyncBrowserScript(Va,Ya)};function Wa(\$a){return \$a.hasAttribute(\"data-phast-params\")}function Xa(_a){return!_a.hasAttribute(\"src\")}};function getSrc(ab){if(ab.hasAttribute(\"data-phast-original-src\")){return ab.getAttribute(\"data-phast-original-src\")}else if(ab.hasAttribute(\"src\")){return ab.getAttribute(\"src\")}}function isAsync(bb){return bb.hasAttribute(\"async\")}function isDefer(cb){return cb.hasAttribute(\"defer\")}\n"));
8859 $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"));
8860 }
8861 private function rewrite(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
8862 {
8863 if ($script->hasAttribute('type')) {
8864 $script->setAttribute('data-phast-original-type', $script->getAttribute('type'));
8865 }
8866 $script->setAttribute('type', 'text/phast');
8867 if ($script->hasAttribute('data-phast-params')) {
8868 $script->removeAttribute('src');
8869 }
8870 }
8871 private function isDeferralDisabled(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
8872 {
8873 return $script->hasAttribute('data-phast-no-defer') || $script->hasAttribute('data-pagespeed-no-defer') || $script->getAttribute('data-cfasync') === 'false' || preg_match('~^\\s*(?<q>[\'"])phast-no-defer\\k<q>~', $script->textContent);
8874 }
8875 }
8876 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags;
8877
8878 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8879 {
8880 public function make(array $config)
8881 {
8882 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));
8883 }
8884 }
8885 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS;
8886
8887 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8888 {
8889 public function make(array $config)
8890 {
8891 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));
8892 }
8893 }
8894 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS;
8895
8896 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
8897 {
8898 /**
8899 * @var ImageURLRewriter
8900 */
8901 protected $rewriter;
8902 /**
8903 * Filter constructor.
8904 * @param ImageURLRewriter $rewriter
8905 */
8906 public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter)
8907 {
8908 $this->rewriter = $rewriter;
8909 }
8910 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
8911 {
8912 if ($tag->hasAttribute('style')) {
8913 $tag->setAttribute('style', $this->rewriter->rewriteStyle($tag->getAttribute('style')));
8914 }
8915 (yield $tag);
8916 }
8917 }
8918 namespace Kibo\Phast\Filters\HTML\PhastScriptsCompiler;
8919
8920 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8921 {
8922 public function make(array $config)
8923 {
8924 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'phast-scripts');
8925 $compiler = new \Kibo\Phast\Filters\HTML\PhastScriptsCompiler\PhastJavaScriptCompiler($cache, $config['servicesUrl'], $config['serviceRequestFormat']);
8926 return new \Kibo\Phast\Filters\HTML\PhastScriptsCompiler\Filter($compiler);
8927 }
8928 }
8929 namespace Kibo\Phast\Filters\HTML\ScriptsProxyService;
8930
8931 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8932 {
8933 public function make(array $config)
8934 {
8935 if (!isset($config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class]['serviceUrl'])) {
8936 $config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class]['serviceUrl'] = $config['servicesUrl'];
8937 }
8938 $filterConfig = $config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class];
8939 $filterConfig['match'] = $config['scripts']['whitelist'];
8940 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));
8941 }
8942 }
8943 namespace Kibo\Phast\Filters\HTML\CSSInlining;
8944
8945 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8946 {
8947 public function make(array $config)
8948 {
8949 $localRetriever = new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']);
8950 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
8951 $retriever->addRetriever($localRetriever);
8952 $retriever->addRetriever(new \Kibo\Phast\Retrievers\CachingRetriever(new \Kibo\Phast\Cache\File\Cache($config['cache'], 'css')));
8953 if (!isset($config['documents']['filters'][\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class]['serviceUrl'])) {
8954 $config['documents']['filters'][\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class]['serviceUrl'] = $config['servicesUrl'];
8955 }
8956 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));
8957 }
8958 }
8959 namespace Kibo\Phast\Filters\HTML\Diagnostics;
8960
8961 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8962 {
8963 public function make(array $config)
8964 {
8965 $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';
8966 return new \Kibo\Phast\Filters\HTML\Diagnostics\Filter($url);
8967 }
8968 }
8969 namespace Kibo\Phast\Filters\Image\Exceptions;
8970
8971 class ImageProcessingException extends \Kibo\Phast\Exceptions\RuntimeException
8972 {
8973 }
8974 namespace Kibo\Phast\Filters\Image\ImageImplementations;
8975
8976 class DummyImage extends \Kibo\Phast\Filters\Image\ImageImplementations\BaseImage implements \Kibo\Phast\Filters\Image\Image
8977 {
8978 /**
8979 * @var string
8980 */
8981 private $imageString;
8982 private $transformationString;
8983 /**
8984 * DummyImage constructor.
8985 *
8986 * @param int $width
8987 * @param int $height
8988 */
8989 public function __construct($width = null, $height = null)
8990 {
8991 $this->width = $width;
8992 $this->height = $height;
8993 }
8994 /**
8995 * @return int
8996 */
8997 public function getWidth()
8998 {
8999 return $this->width;
9000 }
9001 /**
9002 * @return int
9003 */
9004 public function getHeight()
9005 {
9006 return $this->height;
9007 }
9008 /**
9009 * @return string
9010 */
9011 public function getType()
9012 {
9013 return $this->type;
9014 }
9015 /**
9016 * @param string $type
9017 */
9018 public function setType($type)
9019 {
9020 $this->type = $type;
9021 }
9022 /**
9023 * @return int
9024 */
9025 public function getCompression()
9026 {
9027 return $this->compression;
9028 }
9029 /**
9030 * @return string
9031 */
9032 public function getAsString()
9033 {
9034 return $this->imageString;
9035 }
9036 /**
9037 * @param string $imageString
9038 */
9039 public function setImageString($imageString)
9040 {
9041 $this->imageString = $imageString;
9042 }
9043 /**
9044 * @param mixed $transformationString
9045 */
9046 public function setTransformationString($transformationString)
9047 {
9048 $this->transformationString = $transformationString;
9049 }
9050 protected function __clone()
9051 {
9052 $this->imageString = $this->transformationString;
9053 }
9054 }
9055 namespace Kibo\Phast\Filters\Service;
9056
9057 class CachingServiceFilter implements \Kibo\Phast\Services\ServiceFilter
9058 {
9059 use \Kibo\Phast\Logging\LoggingTrait;
9060 /**
9061 * @var Cache
9062 */
9063 private $cache;
9064 /**
9065 * @var CachedResultServiceFilter
9066 */
9067 private $cachedFilter;
9068 /**
9069 * @var Retriever
9070 */
9071 private $retriever;
9072 /**
9073 * CachingServiceFilter constructor.
9074 * @param Cache $cache
9075 * @param CachedResultServiceFilter $cachedFilter
9076 * @param Retriever $retriever
9077 */
9078 public function __construct(\Kibo\Phast\Cache\Cache $cache, \Kibo\Phast\Filters\Service\CachedResultServiceFilter $cachedFilter, \Kibo\Phast\Retrievers\Retriever $retriever)
9079 {
9080 $this->cache = $cache;
9081 $this->cachedFilter = $cachedFilter;
9082 $this->retriever = $retriever;
9083 }
9084 /**
9085 * @param Resource $resource
9086 * @param array $request
9087 * @return Resource
9088 * @throws CachedExceptionException
9089 */
9090 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9091 {
9092 $key = $this->cachedFilter->getCacheSalt($resource, $request);
9093 $this->logger()->info('Trying to get {url} from cache', ['url' => (string) $resource->getUrl()]);
9094 $result = $this->cache->get($key);
9095 if (isset($result['encoding']) && $result['encoding'] != 'identity') {
9096 $result = null;
9097 }
9098 if ($result && $this->checkDependencies($result)) {
9099 return $this->deserializeCachedData($result);
9100 }
9101 try {
9102 $result = $this->cachedFilter->apply($resource, $request);
9103 $this->cache->set($key, $this->serializeResource($result));
9104 return $result;
9105 } catch (\Exception $e) {
9106 $cachingException = $this->serializeException($e);
9107 $this->cache->set($key, $cachingException);
9108 throw $this->deserializeException($cachingException);
9109 }
9110 }
9111 private function checkDependencies(array $data)
9112 {
9113 foreach ((array) @$data['dependencies'] as $dep) {
9114 $url = \Kibo\Phast\ValueObjects\URL::fromString($dep['url']);
9115 if ($this->retriever->getCacheSalt($url) >= $dep['cacheMarker']) {
9116 return false;
9117 }
9118 }
9119 return true;
9120 }
9121 private function deserializeCachedData(array $data)
9122 {
9123 if ($data['dataType'] == 'exception') {
9124 throw $this->deserializeException($data);
9125 }
9126 return $this->deserializeResource($data);
9127 }
9128 private function serializeResource(\Kibo\Phast\ValueObjects\Resource $resource)
9129 {
9130 return ['dataType' => 'resource', 'url' => $resource->getUrl()->toString(), 'mimeType' => $resource->getMimeType(), 'blob' => base64_encode($resource->getContent()), 'dependencies' => $this->serializeDependencies($resource)];
9131 }
9132 private function serializeDependencies(\Kibo\Phast\ValueObjects\Resource $resource)
9133 {
9134 return array_map(function (\Kibo\Phast\ValueObjects\Resource $dep) {
9135 return ['url' => $dep->getUrl()->toString(), 'cacheMarker' => $dep->getCacheSalt()];
9136 }, $resource->getDependencies());
9137 }
9138 private function deserializeResource(array $data)
9139 {
9140 $params = [\Kibo\Phast\ValueObjects\URL::fromString($data['url']), base64_decode($data['blob']), $data['mimeType']];
9141 return \Kibo\Phast\ValueObjects\Resource::makeWithContent(...$params);
9142 }
9143 private function serializeException(\Exception $e)
9144 {
9145 return ['dataType' => 'exception', 'class' => get_class($e), 'msg' => $e->getMessage(), 'code' => $e->getCode()];
9146 }
9147 private function deserializeException(array $data)
9148 {
9149 return new \Kibo\Phast\Exceptions\CachedExceptionException(sprintf('Phast: %s: Type: %s, Msg: %s, Code: %s', static::class, $data['class'], $data['msg'], $data['code']));
9150 }
9151 }
9152 namespace Kibo\Phast\Filters\Service;
9153
9154 interface CachedResultServiceFilter extends \Kibo\Phast\Services\ServiceFilter
9155 {
9156 /**
9157 * @param Resource $resource
9158 * @param array $request
9159 * @return string
9160 */
9161 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request);
9162 }
9163 namespace Kibo\Phast\Filters\Service;
9164
9165 class CompositeFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9166 {
9167 use \Kibo\Phast\Logging\LoggingTrait;
9168 /**
9169 * @var ServiceFilter[]
9170 */
9171 private $filters = array();
9172 public function addFilter(\Kibo\Phast\Services\ServiceFilter $filter)
9173 {
9174 $this->filters[] = $filter;
9175 }
9176 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9177 {
9178 $classes = array_map('get_class', $this->filters);
9179 $cached = array_filter($this->filters, function (\Kibo\Phast\Services\ServiceFilter $filter) {
9180 return $filter instanceof \Kibo\Phast\Filters\Service\CachedResultServiceFilter;
9181 });
9182 $salts = array_map(function (\Kibo\Phast\Filters\Service\CachedResultServiceFilter $filter) use($resource, $request) {
9183 return $filter->getCacheSalt($resource, $request);
9184 }, $cached);
9185 return join("\n", array_merge($classes, $salts, [$resource->getUrl(), $resource->getCacheSalt()]));
9186 }
9187 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9188 {
9189 $this->logger()->info('Starting filtering for resource {url}', ['url' => $resource->getUrl()]);
9190 $result = array_reduce($this->filters, function (\Kibo\Phast\ValueObjects\Resource $resource, \Kibo\Phast\Services\ServiceFilter $filter) use($request) {
9191 $this->logger()->info('Starting {filter}', ['filter' => get_class($filter)]);
9192 try {
9193 return $filter->apply($resource, $request);
9194 } catch (\Kibo\Phast\Exceptions\RuntimeException $e) {
9195 $message = 'Phast RuntimeException: Filter: {filter} Exception: {exceptionClass} Msg: {message} Code: {code} File: {file} Line: {line}';
9196 $this->logger()->critical($message, ['filter' => get_class($filter), 'exceptionClass' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
9197 return $resource;
9198 }
9199 }, $resource);
9200 $this->logger()->info('Done filtering for resource {url}', ['url' => $resource->getUrl()]);
9201 return $result;
9202 }
9203 }
9204 namespace Kibo\Phast\Filters\CSS\CSSMinifier;
9205
9206 class Filter implements \Kibo\Phast\Services\ServiceFilter
9207 {
9208 /**
9209 * @param Resource $resource
9210 * @param array $request
9211 * @return Resource
9212 */
9213 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9214 {
9215 $content = $resource->getContent();
9216 // Normalize whitespace
9217 $content = preg_replace('~\\s+~', ' ', $content);
9218 // Remove whitespace before and after operators
9219 $chars = [',', '{', '}', ';'];
9220 foreach ($chars as $char) {
9221 $content = str_replace("{$char} ", $char, $content);
9222 $content = str_replace(" {$char}", $char, $content);
9223 }
9224 // Remove whitespace after colons
9225 $content = str_replace(': ', ':', $content);
9226 return $resource->withContent(trim($content));
9227 }
9228 }
9229 namespace Kibo\Phast\Filters\CSS\CSSURLRewriter;
9230
9231 class Filter implements \Kibo\Phast\Services\ServiceFilter
9232 {
9233 /**
9234 * @param Resource $resource
9235 * @param array $request
9236 * @return Resource
9237 */
9238 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9239 {
9240 $baseUrl = $resource->getUrl();
9241 $callback = function ($match) use($baseUrl) {
9242 if (preg_match('~^[a-z]+:|^#~i', $match[3])) {
9243 return $match[0];
9244 }
9245 return $match[1] . \Kibo\Phast\ValueObjects\URL::fromString($match[3])->withBase($baseUrl) . $match[4];
9246 };
9247 $cssContent = preg_replace_callback('~
9248 \\b
9249 ( url\\( ([\'"]?) )
9250 ([A-Za-z0-9_/.:?&=+%,#@-]+)
9251 ( \\2 \\) )
9252 ~x', $callback, $resource->getContent());
9253 $cssContent = preg_replace_callback('~
9254 ( @import \\s+ ([\'"]) )
9255 ([A-Za-z0-9_/.:?&=+%,#@-]+)
9256 ( \\2 )
9257 ~x', $callback, $cssContent);
9258 return $resource->withContent($cssContent);
9259 }
9260 }
9261 namespace Kibo\Phast\Filters\CSS\ImageURLRewriter;
9262
9263 class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9264 {
9265 /**
9266 * @var ImageURLRewriter
9267 */
9268 private $rewriter;
9269 /**
9270 * Filter constructor.
9271 * @param ImageURLRewriter $rewriter
9272 */
9273 public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter)
9274 {
9275 $this->rewriter = $rewriter;
9276 }
9277 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9278 {
9279 return $this->rewriter->getCacheSalt();
9280 }
9281 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9282 {
9283 $content = $this->rewriter->rewriteStyle($resource->getContent());
9284 $dependencies = $this->rewriter->getInlinedResources();
9285 return $resource->withContent($content)->withDependencies($dependencies);
9286 }
9287 }
9288 namespace Kibo\Phast\Filters\CSS\Composite;
9289
9290 class Filter extends \Kibo\Phast\Filters\Service\CompositeFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9291 {
9292 public function __construct()
9293 {
9294 $this->addFilter(new \Kibo\Phast\Filters\CSS\CommentsRemoval\Filter());
9295 }
9296 }
9297 namespace Kibo\Phast\Filters\CSS\FontSwap;
9298
9299 class Filter implements \Kibo\Phast\Services\ServiceFilter
9300 {
9301 const FONT_FACE_REGEXP = '/(@font-face\\s*\\{)([^}]*)/i';
9302 const ICON_FONT_FAMILIES = array('Font Awesome', 'GeneratePress', 'Dashicons', 'Ionicons');
9303 private $fontDisplayBlockPattern;
9304 public function __construct()
9305 {
9306 $this->fontDisplayBlockPattern = $this->getFontDisplayBlockPattern();
9307 }
9308 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9309 {
9310 $css = $resource->getContent();
9311 $filtered = preg_replace_callback(self::FONT_FACE_REGEXP, function ($match) {
9312 list($block, $start, $contents) = $match;
9313 $mode = preg_match($this->fontDisplayBlockPattern, $contents) ? 'block' : 'swap';
9314 return $start . 'font-display:' . $mode . ';' . $contents;
9315 }, $css);
9316 return $resource->withContent($filtered);
9317 }
9318 private function getFontDisplayBlockPattern()
9319 {
9320 $patterns = [];
9321 foreach (self::ICON_FONT_FAMILIES as $family) {
9322 $chars = str_split($family);
9323 $chars = array_map(function ($char) {
9324 return preg_quote($char, '~');
9325 }, $chars);
9326 $patterns[] = implode('\\s*', $chars);
9327 }
9328 return '~' . implode('|', $patterns) . '~i';
9329 }
9330 }
9331 namespace Kibo\Phast\Filters\CSS\ImportsStripper;
9332
9333 class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9334 {
9335 use \Kibo\Phast\Logging\LoggingTrait;
9336 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9337 {
9338 return $this->shouldStripImports($request) ? 'strip-imports' : 'no-strip-imports';
9339 }
9340 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9341 {
9342 if (!$this->shouldStripImports($request)) {
9343 $this->logger()->info('No import stripping requested! Skipping!');
9344 return $resource;
9345 }
9346 $css = $resource->getContent();
9347 $stripped = preg_replace(\Kibo\Phast\Filters\HTML\CSSInlining\Filter::CSS_IMPORTS_REGEXP, '', $css);
9348 return $resource->withContent($stripped);
9349 }
9350 private function shouldStripImports(array $request)
9351 {
9352 return isset($request['strip-imports']);
9353 }
9354 }
9355 namespace Kibo\Phast\Filters\CSS\CommentsRemoval;
9356
9357 class Filter implements \Kibo\Phast\Services\ServiceFilter
9358 {
9359 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9360 {
9361 $content = preg_replace('~/\\*[^*]*\\*+([^/*][^*]*\\*+)*/~', '', $resource->getContent());
9362 return $resource->withContent($content);
9363 }
9364 }
9365 namespace Kibo\Phast\Filters\Text\Decode;
9366
9367 class Filter implements \Kibo\Phast\Services\ServiceFilter
9368 {
9369 const UTF8_BOM = "\357\273\277";
9370 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request = array())
9371 {
9372 $content = $resource->getContent();
9373 if (substr($content, 0, strlen(self::UTF8_BOM)) == self::UTF8_BOM) {
9374 $content = substr($content, strlen(self::UTF8_BOM));
9375 }
9376 return $resource->withContent($content);
9377 }
9378 }
9379 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
9380
9381 class ClosingTag extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element
9382 {
9383 /**
9384 * @var string
9385 */
9386 private $tagName;
9387 /**
9388 * ClosingTag constructor.
9389 * @param string $tagName
9390 */
9391 public function __construct($tagName)
9392 {
9393 $this->tagName = strtolower($tagName);
9394 }
9395 /**
9396 * @return string
9397 */
9398 public function getTagName()
9399 {
9400 return $this->tagName;
9401 }
9402 public function appendChild(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element $element)
9403 {
9404 $this->stream->insertBeforeElement($this, $element);
9405 }
9406 public function dumpValue()
9407 {
9408 return $this->tagName;
9409 }
9410 }
9411 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
9412
9413 class Tag extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element
9414 {
9415 /**
9416 * @var string
9417 */
9418 private $tagName;
9419 /**
9420 * @var array
9421 */
9422 private $attributes = array();
9423 /**
9424 * @var array
9425 */
9426 private $newAttributes = array();
9427 /**
9428 * @var \Iterator
9429 */
9430 private $attributeReader;
9431 /**
9432 * @var string
9433 */
9434 private $textContent = '';
9435 /**
9436 * @var string
9437 */
9438 private $closingTag = '';
9439 private $dirty = false;
9440 /**
9441 * Tag constructor.
9442 * @param $tagName
9443 * @param array|\Traversable $attributes
9444 */
9445 public function __construct($tagName, $attributes = array())
9446 {
9447 $this->tagName = strtolower($tagName);
9448 if ($attributes instanceof \Iterator) {
9449 $this->attributeReader = $attributes;
9450 } elseif (is_array($attributes)) {
9451 $this->attributeReader = new \ArrayIterator($attributes);
9452 } else {
9453 throw new \InvalidArgumentException('Attributes must be array or Iterator');
9454 }
9455 }
9456 /**
9457 * @return string
9458 */
9459 public function getTagName()
9460 {
9461 return $this->tagName;
9462 }
9463 /**
9464 * @param string $attrName
9465 * @return bool
9466 */
9467 public function hasAttribute($attrName)
9468 {
9469 return $this->getAttribute($attrName) !== null;
9470 }
9471 /**
9472 * @param string $attrName
9473 * @return mixed|null
9474 */
9475 public function getAttribute($attrName)
9476 {
9477 if (array_key_exists($attrName, $this->newAttributes)) {
9478 return $this->newAttributes[$attrName];
9479 }
9480 if (!array_key_exists($attrName, $this->attributes)) {
9481 $this->readUntilAttribute($attrName);
9482 }
9483 if (isset($this->attributes[$attrName])) {
9484 return $this->attributes[$attrName];
9485 }
9486 }
9487 /** @return string[] */
9488 public function getAttributes()
9489 {
9490 $this->readUntilAttribute(null);
9491 return array_filter($this->newAttributes + $this->attributes, function ($value) {
9492 return $value !== null;
9493 });
9494 }
9495 private function readUntilAttribute($attrName)
9496 {
9497 if (!$this->attributeReader) {
9498 return;
9499 }
9500 while ($this->attributeReader->valid()) {
9501 $name = strtolower($this->attributeReader->key());
9502 $value = $this->attributeReader->current();
9503 $this->attributeReader->next();
9504 if (!isset($this->attributes[$name])) {
9505 $this->attributes[$name] = $value;
9506 }
9507 if ($name == $attrName) {
9508 return;
9509 }
9510 }
9511 $this->attributeReader = null;
9512 }
9513 /**
9514 * @param string $attrName
9515 * @param string $value
9516 */
9517 public function setAttribute($attrName, $value)
9518 {
9519 if ($this->getAttribute($attrName) === $value) {
9520 return;
9521 }
9522 $this->dirty = true;
9523 $this->newAttributes[$attrName] = $value;
9524 }
9525 /**
9526 * @param string $attrName
9527 */
9528 public function removeAttribute($attrName)
9529 {
9530 $this->dirty = true;
9531 $this->newAttributes[$attrName] = null;
9532 }
9533 /**
9534 * @return string
9535 */
9536 public function getTextContent()
9537 {
9538 return $this->textContent;
9539 }
9540 /**
9541 * @param string $textContent
9542 */
9543 public function setTextContent($textContent)
9544 {
9545 $this->textContent = $textContent;
9546 }
9547 /**
9548 * @param $closingTag
9549 * @return Tag
9550 */
9551 public function withClosingTag($closingTag)
9552 {
9553 $new = clone $this;
9554 $new->closingTag = $closingTag;
9555 return $new;
9556 }
9557 /**
9558 * @return string
9559 */
9560 public function getClosingTag()
9561 {
9562 return $this->closingTag;
9563 }
9564 public function __toString()
9565 {
9566 return $this->getOpening() . $this->textContent . $this->getClosing();
9567 }
9568 private function getOpening()
9569 {
9570 if ($this->dirty || !isset($this->originalString)) {
9571 return $this->generateOpeningTag();
9572 }
9573 return parent::__toString();
9574 }
9575 private function getClosing()
9576 {
9577 if ($this->closingTag) {
9578 return $this->closingTag;
9579 }
9580 if ($this->mustHaveClosing() && !$this->isFromParser()) {
9581 return '</' . $this->tagName . '>';
9582 }
9583 return '';
9584 }
9585 private function generateOpeningTag()
9586 {
9587 $parts = ['<' . $this->tagName];
9588 foreach ($this->getAttributes() as $name => $value) {
9589 $parts[] = $this->generateAttribute($name, $value);
9590 }
9591 return join(' ', $parts) . '>';
9592 }
9593 private function generateAttribute($name, $value)
9594 {
9595 $result = $name;
9596 if ($value != '') {
9597 $result .= '=' . $this->quoteAttributeValue($value);
9598 }
9599 return $result;
9600 }
9601 private function quoteAttributeValue($value)
9602 {
9603 if (strpos($value, '"') === false) {
9604 return '"' . htmlspecialchars($value) . '"';
9605 }
9606 return "'" . str_replace(['&', "'"], ['&amp;', '&#039;'], $value) . "'";
9607 }
9608 private function mustHaveClosing()
9609 {
9610 return !\Kibo\Phast\Parsing\HTML\HTMLInfo::isA($this->tagName, \Kibo\Phast\Parsing\HTML\HTMLInfo::VOID_TAG);
9611 }
9612 private function isFromParser()
9613 {
9614 return isset($this->originalString);
9615 }
9616 public function dumpValue()
9617 {
9618 $o = $this->tagName;
9619 foreach ($this->attributes as $name => $_) {
9620 $o .= " {$name}=\"" . $this->getAttribute($name) . '"';
9621 }
9622 if ($this->textContent) {
9623 $o .= " content=[{$this->textContent}]";
9624 }
9625 return $o;
9626 }
9627 }
9628 namespace Kibo\Phast\Logging\LogWriters\Dummy;
9629
9630 class Writer implements \Kibo\Phast\Logging\LogWriter
9631 {
9632 public function setLevelMask($mask)
9633 {
9634 }
9635 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry)
9636 {
9637 }
9638 }
9639 namespace Kibo\Phast\Logging\LogWriters;
9640
9641 abstract class BaseLogWriter implements \Kibo\Phast\Logging\LogWriter
9642 {
9643 protected $levelMask = ~0;
9644 protected abstract function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry);
9645 public function setLevelMask($mask)
9646 {
9647 $this->levelMask = $mask;
9648 }
9649 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry)
9650 {
9651 if ($this->levelMask & $entry->getLevel()) {
9652 $this->doWriteEntry($entry);
9653 }
9654 }
9655 }
9656 namespace Kibo\Phast\Services\Css;
9657
9658 class Service extends \Kibo\Phast\Services\BaseService
9659 {
9660 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9661 {
9662 $response = parent::makeResponse($resource, $request);
9663 $response->setHeader('Content-Type', 'text/css');
9664 return $response;
9665 }
9666 }
9667 namespace Kibo\Phast\Services\Scripts;
9668
9669 class Service extends \Kibo\Phast\Services\BaseService
9670 {
9671 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9672 {
9673 $response = parent::makeResponse($resource, $request);
9674 $response->setHeader('Content-Type', 'application/javascript');
9675 return $response;
9676 }
9677 }
9678 namespace Kibo\Phast\Services\Images;
9679
9680 class Service extends \Kibo\Phast\Services\BaseService
9681 {
9682 protected function getParams(\Kibo\Phast\Services\ServiceRequest $request)
9683 {
9684 $params = parent::getParams($request);
9685 if ($this->proxySupportsAccept($request->getHTTPRequest())) {
9686 $params['varyAccept'] = true;
9687 if ($this->browserSupportsWebp($request->getHTTPRequest())) {
9688 $params['preferredType'] = \Kibo\Phast\Filters\Image\Image::TYPE_WEBP;
9689 \Kibo\Phast\Logging\Log::info('WebP will be served if possible!');
9690 }
9691 }
9692 return $params;
9693 }
9694 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9695 {
9696 $response = parent::makeResponse($resource, $request);
9697 $srcUrl = $resource->getUrl();
9698 $response->setHeader('Link', "<{$srcUrl}>; rel=\"canonical\"");
9699 $response->setHeader('Content-Type', $resource->getMimeType());
9700 if ($resource->getMimeType() != \Kibo\Phast\Filters\Image\Image::TYPE_PNG && @$request['varyAccept']) {
9701 $response->setHeader('Vary', 'Accept');
9702 }
9703 return $response;
9704 }
9705 protected function validateIntegrity(\Kibo\Phast\Services\ServiceRequest $request)
9706 {
9707 if (!$this->config['images']['api-mode']) {
9708 parent::validateIntegrity($request);
9709 }
9710 }
9711 protected function validateWhitelisted(\Kibo\Phast\Services\ServiceRequest $request)
9712 {
9713 if (!$this->config['images']['api-mode']) {
9714 parent::validateWhitelisted($request);
9715 }
9716 }
9717 private function browserSupportsWebp(\Kibo\Phast\HTTP\Request $request)
9718 {
9719 return strpos($request->getHeader('accept'), 'image/webp') !== false;
9720 }
9721 private function proxySupportsAccept(\Kibo\Phast\HTTP\Request $request)
9722 {
9723 return !$request->isCloudflare();
9724 }
9725 }
9726 namespace Kibo\PhastPlugins\SDK\AdminPanel;
9727
9728 class DefaultInstallNoticeRenderer implements \Kibo\PhastPlugins\SDK\AdminPanel\InstallNoticeRenderer
9729 {
9730 public function render($notice, $onCloseJSFunction)
9731 {
9732 return $notice;
9733 }
9734 }
9735 namespace Kibo\PhastPlugins\SDK;
9736
9737 interface PluginHost extends \Kibo\PhastPlugins\SDK\ServiceHost
9738 {
9739 /**
9740 * The name of the plugin used for displaying to the users
9741 *
9742 * @return string
9743 */
9744 public function getPluginName();
9745 /**
9746 * The name of the host system
9747 *
9748 * @return string
9749 */
9750 public function getPluginHostName();
9751 /**
9752 * The version of the plugin
9753 *
9754 * @return string
9755 */
9756 public function getPluginHostVersion();
9757 /**
9758 * Tells whether we are in production or development mode.
9759 * In development mode static files will be loaded from a dev server.
9760 * In production mode static files will be loaded from a prebuilt source.
9761 *
9762 * @return bool - TRUE for development, FALSE for production
9763 */
9764 public function isDev();
9765 /**
9766 * @return KeyValueStore
9767 */
9768 public function getKeyValueStore();
9769 /**
9770 * @return InstallNoticeRenderer
9771 */
9772 public function getInstallNoticeRenderer();
9773 /**
9774 * @return HostURLs
9775 */
9776 public function getHostURLs();
9777 /**
9778 * @return Nonce
9779 */
9780 public function getNonce();
9781 /**
9782 * @return NonceChecker
9783 */
9784 public function getNonceChecker();
9785 /**
9786 * @return PhastUser
9787 */
9788 public function getPhastUser();
9789 /**
9790 * Called right after phast's configuration
9791 * has been loaded. Use it to modify the config
9792 * and take any other needed action before
9793 * the filters are applied.
9794 *
9795 * @param array $config - The configuration that has been loaded
9796 * @return array - The configuration to use for phast
9797 */
9798 public function onPhastConfigurationLoad(array $config);
9799 /**
9800 * Returns the current system locale
9801 *
9802 * @return string
9803 */
9804 public function getLocale();
9805 }
9806 namespace Kibo\Phast\Filters\JavaScript\Minification;
9807
9808 class JSMinifierFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9809 {
9810 const VERSION = 2;
9811 private $removeLicenseHeaders = true;
9812 /**
9813 * JSMinifierFilter constructor.
9814 * @param bool $removeLicenseHeaders
9815 */
9816 public function __construct($removeLicenseHeaders)
9817 {
9818 $this->removeLicenseHeaders = (bool) $removeLicenseHeaders;
9819 }
9820 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9821 {
9822 return http_build_query(['v' => self::VERSION, 'removeLicenseHeaders' => $this->removeLicenseHeaders]);
9823 }
9824 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9825 {
9826 $minified = (new \Kibo\Phast\Common\JSMinifier($resource->getContent(), $this->removeLicenseHeaders))->min();
9827 return $resource->withContent($minified);
9828 }
9829 }
9830 namespace Kibo\Phast\Filters\Image\Composite;
9831
9832 class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9833 {
9834 use \Kibo\Phast\Logging\LoggingTrait;
9835 /**
9836 * @var ImageFactory
9837 */
9838 private $imageFactory;
9839 /**
9840 * @var ImageInliningManager
9841 */
9842 private $inliningManager;
9843 /**
9844 * @var ImageFilter[]
9845 */
9846 private $filters = array();
9847 /**
9848 * Filter constructor.
9849 * @param ImageFactory $imageFactory
9850 * @param ImageInliningManager $inliningManager
9851 */
9852 public function __construct(\Kibo\Phast\Filters\Image\ImageFactory $imageFactory, \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManager $inliningManager)
9853 {
9854 $this->imageFactory = $imageFactory;
9855 $this->inliningManager = $inliningManager;
9856 }
9857 public function addImageFilter(\Kibo\Phast\Filters\Image\ImageFilter $filter)
9858 {
9859 $this->filters[] = $filter;
9860 }
9861 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9862 {
9863 $filters = array_map('get_class', $this->filters);
9864 $salts = array_map(function (\Kibo\Phast\Filters\Image\ImageFilter $filter) use($request) {
9865 return $filter->getCacheSalt($request);
9866 }, $this->filters);
9867 return implode("\n", array_merge($filters, $salts, [$this->inliningManager->getMaxImageInliningSize(), $resource->getUrl(), $resource->getCacheSalt()]));
9868 }
9869 /**
9870 * @param Resource $resource
9871 * @param array $request
9872 * @return Resource
9873 */
9874 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9875 {
9876 $image = $this->imageFactory->getForResource($resource);
9877 $filteredImage = $image;
9878 foreach ($this->filters as $filter) {
9879 $this->logger()->info('Applying {filter}', ['filter' => get_class($filter)]);
9880 try {
9881 $filteredImage = $filter->transformImage($filteredImage, $request);
9882 } catch (\Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException $e) {
9883 $message = 'Image filter exception: Filter: {filter} Exception: {exceptionClass} Msg: {message} Code: {code} File: {file} Line: {line}';
9884 $this->logger()->critical($message, ['filter' => get_class($filter), 'exceptionClass' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
9885 }
9886 }
9887 $sizeBefore = $filteredImage->getSizeAsString();
9888 $sizeAfter = $image->getSizeAsString();
9889 $sizeDifference = $sizeBefore - $sizeAfter;
9890 $this->logger()->info('Image processed. Size before/after: {sizeBefore}/{sizeAfter} ({sizeDifference})', ['sizeBefore' => $sizeBefore, 'sizeAfter' => $sizeAfter, 'sizeDifference' => $sizeDifference < 0 ? $sizeDifference : "+{$sizeDifference}"]);
9891 if ($sizeDifference < 0) {
9892 $this->logger()->info('Return filtered image and save {sizeDifference} bytes', ['sizeDifference' => -$sizeDifference]);
9893 $image = $filteredImage;
9894 } else {
9895 $this->logger()->info('Return original image');
9896 }
9897 $processedResource = $resource->withContent($image->getAsString(), $image->getType());
9898 $this->inliningManager->maybeStoreForInlining($processedResource);
9899 return $processedResource;
9900 }
9901 }
9902 namespace Kibo\Phast\Logging\LogWriters\JSONLFile;
9903
9904 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
9905 {
9906 use \Kibo\Phast\Logging\Common\JSONLFileLogTrait;
9907 /**
9908 * @param LogEntry $entry
9909 */
9910 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
9911 {
9912 $encoded = @\Kibo\Phast\Common\JSON::encode($entry->toArray());
9913 if ($encoded) {
9914 $this->makeDirIfNotExists();
9915 @file_put_contents($this->filename, $encoded . "\n", FILE_APPEND | LOCK_EX);
9916 }
9917 }
9918 private function makeDirIfNotExists()
9919 {
9920 if (!@file_exists($this->dir)) {
9921 @mkdir($this->dir, 0777, true);
9922 }
9923 }
9924 }
9925 namespace Kibo\Phast\Logging\LogWriters\Composite;
9926
9927 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
9928 {
9929 /**
9930 * @var Writer[]
9931 */
9932 private $writers = array();
9933 public function addWriter(\Kibo\Phast\Logging\LogWriter $writer)
9934 {
9935 $this->writers[] = $writer;
9936 }
9937 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
9938 {
9939 foreach ($this->writers as $writer) {
9940 $writer->writeEntry($entry);
9941 }
9942 }
9943 }
9944 namespace Kibo\Phast\Logging\LogWriters\RotatingTextFile;
9945
9946 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
9947 {
9948 /** @var string */
9949 private $path = 'phast.log';
9950 /** @var int */
9951 private $maxFiles = 2;
9952 /** @var int */
9953 private $maxSize = 10 * 1024 * 1024;
9954 /** @var ObjectifiedFunctions */
9955 private $funcs;
9956 /**
9957 * @param array $config
9958 * @param ?ObjectifiedFunctions $funcs
9959 */
9960 public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
9961 {
9962 if (isset($config['path'])) {
9963 $this->path = (string) $config['path'];
9964 }
9965 if (isset($config['maxFiles'])) {
9966 $this->maxFiles = (int) $config['maxFiles'];
9967 }
9968 if (isset($config['maxSize'])) {
9969 $this->maxSize = (int) $config['maxSize'];
9970 }
9971 $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs;
9972 }
9973 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
9974 {
9975 if (!($this->levelMask & $entry->getLevel())) {
9976 return;
9977 }
9978 $message = $this->interpolate($entry->getMessage(), $entry->getContext());
9979 $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);
9980 clearstatcache(true, $this->path);
9981 $this->rotate(strlen($line));
9982 file_put_contents($this->path, $line, FILE_APPEND);
9983 }
9984 private function interpolate($message, $context)
9985 {
9986 $prefix = '';
9987 $prefixKeys = ['requestId', 'service', 'class', 'method', 'line'];
9988 foreach ($prefixKeys as $key) {
9989 if (isset($context[$key])) {
9990 $prefix .= '{' . $key . "}\t";
9991 }
9992 }
9993 return preg_replace_callback('/{(.+?)}/', function ($match) use($context) {
9994 return array_key_exists($match[1], $context) ? $context[$match[1]] : $match[0];
9995 }, $prefix . $message);
9996 }
9997 private function rotate($bufferSize)
9998 {
9999 if (!$this->shouldRotate($bufferSize)) {
10000 return;
10001 }
10002 if (!($fp = fopen($this->path, 'r+'))) {
10003 return;
10004 }
10005 try {
10006 if (!flock($fp, LOCK_EX | LOCK_NB)) {
10007 return;
10008 }
10009 if (!$this->shouldRotate($bufferSize)) {
10010 return;
10011 }
10012 for ($i = $this->maxFiles - 1; $i > 0; $i--) {
10013 @rename($this->getName($i - 1), $this->getName($i));
10014 }
10015 } finally {
10016 fclose($fp);
10017 }
10018 }
10019 private function getName($index)
10020 {
10021 if ($index <= 0) {
10022 return $this->path;
10023 }
10024 return $this->path . '.' . $index;
10025 }
10026 private function shouldRotate($bufferSize)
10027 {
10028 $currentSize = @filesize($this->path);
10029 if (!$currentSize) {
10030 return false;
10031 }
10032 $newSize = $currentSize + $bufferSize;
10033 return $newSize > $this->maxSize;
10034 }
10035 }
10036 namespace Kibo\Phast\Logging\LogWriters\PHPError;
10037
10038 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
10039 {
10040 private $messageType = 0;
10041 private $destination = null;
10042 private $extraHeaders = null;
10043 /**
10044 * @var ObjectifiedFunctions
10045 */
10046 private $funcs;
10047 /**
10048 * PHPErrorLogWriter constructor.
10049 * @param array $config
10050 * @param ObjectifiedFunctions $funcs
10051 */
10052 public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
10053 {
10054 foreach (['messageType', 'destination', 'extraHeaders'] as $field) {
10055 if (isset($config[$field])) {
10056 $this->{$field} = $config[$field];
10057 }
10058 }
10059 $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs;
10060 }
10061 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
10062 {
10063 if ($this->levelMask & $entry->getLevel()) {
10064 $this->funcs->error_log($this->interpolate($entry->getMessage(), $entry->getContext()), $this->messageType, $this->destination, $this->extraHeaders);
10065 }
10066 }
10067 private function interpolate($message, $context)
10068 {
10069 $prefix = '';
10070 $prefixKeys = ['requestId', 'service', 'class', 'method', 'line'];
10071 foreach ($prefixKeys as $key) {
10072 if (isset($context[$key])) {
10073 $prefix .= '{' . $key . "}\t";
10074 }
10075 }
10076 return preg_replace_callback('/{(.+?)}/', function ($match) use($context) {
10077 return array_key_exists($match[1], $context) ? $context[$match[1]] : $match[0];
10078 }, $prefix . $message);
10079 }
10080 }