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

10,070 lines 395.9 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|null
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', '!doctype'])) {
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=function(){if(!(\"FontFace\"in window)){return false}var b=new FontFace(\"t\",'url( \"data:font/woff2;base64,d09GMgABAAAAAADwAAoAAAAAAiQAAACoAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAALAogOAE2AiQDBgsGAAQgBSAHIBuDAciO1EZ3I/mL5/+5/rfPnTt9/9Qa8H4cUUZxaRbh36LiKJoVh61XGzw6ufkpoeZBW4KphwFYIJGHB4LAY4hby++gW+6N1EN94I49v86yCpUdYgqeZrOWN34CMQg2tAmthdli0eePIwAKNIIRS4AGZFzdX9lbBUAQlm//f262/61o8PlYO/D1/X4FrWFFgdCQD9DpGJSxmFyjOAGUU4P0qigcNb82GAAA\" ) format( \"woff2\" )',{});b.load()[\"catch\"](function(){});return b.status==\"loading\"||b.status==\"loaded\"}();if(a){return}console.log(\"[Phast] Browser does not support WOFF2, falling back to original stylesheets\");Array.prototype.forEach.call(document.querySelectorAll(\"style[data-phast-ie-fallback-url]\"),function(c){var d=document.createElement(\"link\");if(c.hasAttribute(\"media\")){d.setAttribute(\"media\",c.getAttribute(\"media\"))}d.setAttribute(\"rel\",\"stylesheet\");d.setAttribute(\"href\",c.getAttribute(\"data-phast-ie-fallback-url\"));c.parentNode.insertBefore(d,c);c.parentNode.removeChild(c)});Array.prototype.forEach.call(document.querySelectorAll(\"style[data-phast-nested-inlined]\"),function(e){e.parentNode.removeChild(e)})})();\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\\b[^<>]*> |
4800 \\s*+ <html\\b[^<>]*> |
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_get_level() && @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 $pass = isset($this->pass) ? ':' . $this->pass : '';
5331 $pass = isset($this->user) || isset($this->pass) ? "{$pass}@" : '';
5332 return $this->encodeSpecialCharacters(implode('', [isset($this->scheme) ? $this->scheme . '://' : '', $this->user, $pass, $this->host, isset($this->port) ? ':' . $this->port : '', $this->path, isset($this->query) ? '?' . $this->query : '', isset($this->fragment) ? '#' . $this->fragment : '']));
5333 }
5334 private function encodeSpecialCharacters($string)
5335 {
5336 return preg_replace_callback('~[^' . preg_quote('!#$&\'()*+,/:;=?@[]', '~') . preg_quote('-_.~', '~') . 'A-Za-z0-9%' . ']~', function ($match) {
5337 return rawurlencode($match[0]);
5338 }, $string);
5339 }
5340 private function normalizePath($path)
5341 {
5342 $stack = [];
5343 $head = null;
5344 foreach (explode('/', $path) as $part) {
5345 if ($part == '.' || $part == '') {
5346 continue;
5347 }
5348 if (!is_null($head) && $part == '..' && $head != '..') {
5349 array_pop($stack);
5350 $head = empty($stack) ? null : $stack[count($stack) - 1];
5351 } else {
5352 $stack[] = $head = $part;
5353 }
5354 }
5355 $normalized = substr($path, 0, 1) == '/' ? '/' : '';
5356 if (!empty($stack)) {
5357 $normalized .= join('/', $stack);
5358 $normalized .= substr($path, -1) == '/' ? '/' : '';
5359 }
5360 return $normalized;
5361 }
5362 private function resolvePath($base, $requested)
5363 {
5364 if (!$requested) {
5365 return $base;
5366 }
5367 if ($requested[0] == '/') {
5368 return $requested;
5369 }
5370 if (substr($base, -1, 1) == '/') {
5371 $usedBase = $base;
5372 } else {
5373 $usedBase = dirname($base);
5374 }
5375 return rtrim($usedBase, '/') . '/' . $requested;
5376 }
5377 /**
5378 * @return string
5379 */
5380 public function getScheme()
5381 {
5382 return $this->scheme;
5383 }
5384 /**
5385 * @return string
5386 */
5387 public function getHost()
5388 {
5389 return $this->host;
5390 }
5391 /**
5392 * @return string
5393 */
5394 public function getPort()
5395 {
5396 return $this->port;
5397 }
5398 /**
5399 * @return string
5400 */
5401 public function getUser()
5402 {
5403 return $this->user;
5404 }
5405 /**
5406 * @return string
5407 */
5408 public function getPass()
5409 {
5410 return $this->pass;
5411 }
5412 /** @return string */
5413 public function getDecodedPath()
5414 {
5415 return urldecode($this->path);
5416 }
5417 /**
5418 * @return string
5419 */
5420 public function getPath()
5421 {
5422 return $this->path;
5423 }
5424 /**
5425 * @return string
5426 */
5427 public function getQuery()
5428 {
5429 return $this->query;
5430 }
5431 /**
5432 * @return string
5433 */
5434 public function getExtension()
5435 {
5436 $matches = [];
5437 if (preg_match('/\\.([^.]*)$/', $this->path, $matches)) {
5438 return $matches[1];
5439 }
5440 return '';
5441 }
5442 /**
5443 * @return string
5444 */
5445 public function getFragment()
5446 {
5447 return $this->fragment;
5448 }
5449 /**
5450 * @param string $path
5451 * @return self
5452 */
5453 public function withPath($path)
5454 {
5455 $url = clone $this;
5456 $url->path = (string) $path;
5457 return $url;
5458 }
5459 /**
5460 * @param string|null $query
5461 * @return self
5462 */
5463 public function withQuery($query)
5464 {
5465 $url = clone $this;
5466 if ($query === null) {
5467 $url->query = null;
5468 } else {
5469 $url->query = (string) $query;
5470 }
5471 return $url;
5472 }
5473 /**
5474 * @return self
5475 */
5476 public function withoutQuery()
5477 {
5478 $url = clone $this;
5479 $url->query = null;
5480 return $url;
5481 }
5482 public function __toString()
5483 {
5484 return $this->toString();
5485 }
5486 public function rewrite(\Kibo\Phast\ValueObjects\URL $from, \Kibo\Phast\ValueObjects\URL $to)
5487 {
5488 $str_from = rtrim($from->toString(), '/');
5489 $str_to = rtrim($to->toString(), '/');
5490 return \Kibo\Phast\ValueObjects\URL::fromString(preg_replace('~^' . preg_quote($str_from, '~') . '(?=$|/)~', $str_to, $this->toString()));
5491 }
5492 }
5493 namespace Kibo\Phast\Logging;
5494
5495 class LogLevel
5496 {
5497 const EMERGENCY = 128;
5498 const ALERT = 64;
5499 const CRITICAL = 32;
5500 const ERROR = 16;
5501 const WARNING = 8;
5502 const NOTICE = 4;
5503 const INFO = 2;
5504 const DEBUG = 1;
5505 public static function toString($level)
5506 {
5507 switch ($level) {
5508 case self::EMERGENCY:
5509 return 'EMERGENCY';
5510 case self::ALERT:
5511 return 'ALERT';
5512 case self::CRITICAL:
5513 return 'CRITICAL';
5514 case self::ERROR:
5515 return 'ERROR';
5516 case self::WARNING:
5517 return 'WARNING';
5518 case self::NOTICE:
5519 return 'NOTICE';
5520 case self::INFO:
5521 return 'INFO';
5522 case self::DEBUG:
5523 return 'DEBUG';
5524 default:
5525 return 'UNKNOWN';
5526 }
5527 }
5528 }
5529 namespace Kibo\Phast\Logging;
5530
5531 class Log
5532 {
5533 /**
5534 * @var Logger
5535 */
5536 private static $logger;
5537 public static function setLogger(\Kibo\Phast\Logging\Logger $logger)
5538 {
5539 self::$logger = $logger;
5540 }
5541 public static function initWithDummy()
5542 {
5543 self::$logger = new \Kibo\Phast\Logging\Logger(new \Kibo\Phast\Logging\LogWriters\Dummy\Writer());
5544 }
5545 public static function init(array $config, \Kibo\Phast\Services\ServiceRequest $request, $service)
5546 {
5547 $writer = (new \Kibo\Phast\Logging\LogWriters\Factory())->make($config, $request);
5548 $logger = new \Kibo\Phast\Logging\Logger($writer);
5549 self::$logger = $logger->withContext(['documentRequestId' => $request->getDocumentRequestId(), 'requestId' => mt_rand(0, 99999999), 'service' => $service]);
5550 }
5551 /**
5552 * @return Logger
5553 */
5554 public static function get()
5555 {
5556 if (!isset(self::$logger)) {
5557 self::initWithDummy();
5558 }
5559 return self::$logger;
5560 }
5561 /**
5562 * @param array $context
5563 * @return Logger
5564 */
5565 public static function context(array $context)
5566 {
5567 return self::get()->withContext($context);
5568 }
5569 /**
5570 * System is unusable.
5571 *
5572 * @param string $message
5573 * @param array $context
5574 *
5575 * @return void
5576 */
5577 public static function emergency($message, array $context = array())
5578 {
5579 self::get()->emergency($message, $context);
5580 }
5581 /**
5582 * Action must be taken immediately.
5583 *
5584 * Example: Entire website down, database unavailable, etc. This should
5585 * trigger the SMS alerts and wake you up.
5586 *
5587 * @param string $message
5588 * @param array $context
5589 *
5590 * @return void
5591 */
5592 public static function alert($message, array $context = array())
5593 {
5594 self::get()->alert($message, $context);
5595 }
5596 /**
5597 * Critical conditions.
5598 *
5599 * Example: Application component unavailable, unexpected exception.
5600 *
5601 * @param string $message
5602 * @param array $context
5603 *
5604 * @return void
5605 */
5606 public static function critical($message, array $context = array())
5607 {
5608 self::get()->critical($message, $context);
5609 }
5610 /**
5611 * Runtime errors that do not require immediate action but should typically
5612 * be logged and monitored.
5613 *
5614 * @param string $message
5615 * @param array $context
5616 *
5617 * @return void
5618 */
5619 public static function error($message, array $context = array())
5620 {
5621 self::get()->error($message, $context);
5622 }
5623 /**
5624 * Exceptional occurrences that are not errors.
5625 *
5626 * Example: Use of deprecated APIs, poor use of an API, undesirable things
5627 * that are not necessarily wrong.
5628 *
5629 * @param string $message
5630 * @param array $context
5631 *
5632 * @return void
5633 */
5634 public static function warning($message, array $context = array())
5635 {
5636 self::get()->warning($message, $context);
5637 }
5638 /**
5639 * Normal but significant events.
5640 *
5641 * @param string $message
5642 * @param array $context
5643 *
5644 * @return void
5645 */
5646 public static function notice($message, array $context = array())
5647 {
5648 self::get()->notice($message, $context);
5649 }
5650 /**
5651 * Interesting events.
5652 *
5653 * Example: User logs in, SQL logs.
5654 *
5655 * @param string $message
5656 * @param array $context
5657 *
5658 * @return void
5659 */
5660 public static function info($message, array $context = array())
5661 {
5662 self::get()->info($message, $context);
5663 }
5664 /**
5665 * Detailed debug information.
5666 *
5667 * @param string $message
5668 * @param array $context
5669 *
5670 * @return void
5671 */
5672 public static function debug($message, array $context = array())
5673 {
5674 self::get()->debug($message, $context);
5675 }
5676 }
5677 namespace Kibo\Phast\Logging\LogWriters;
5678
5679 class Factory
5680 {
5681 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5682 {
5683 if (isset($config['logWriters']) && count($config['logWriters']) > 1) {
5684 $class = \Kibo\Phast\Logging\LogWriters\Composite\Writer::class;
5685 } elseif (isset($config['logWriters'])) {
5686 $config = array_pop($config['logWriters']);
5687 $class = $config['class'];
5688 } else {
5689 $class = $config['class'];
5690 }
5691 $package = \Kibo\Phast\Environment\Package::fromPackageClass($class);
5692 $writer = $package->getFactory()->make($config, $request);
5693 if (isset($config['levelMask'])) {
5694 $writer->setLevelMask($config['levelMask']);
5695 }
5696 return $writer;
5697 }
5698 }
5699 namespace Kibo\Phast\Logging\LogWriters\JSONLFile;
5700
5701 class Factory
5702 {
5703 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5704 {
5705 return new \Kibo\Phast\Logging\LogWriters\JSONLFile\Writer($config['logRoot'], $request->getDocumentRequestId());
5706 }
5707 }
5708 namespace Kibo\Phast\Logging\LogWriters\Composite;
5709
5710 class Factory
5711 {
5712 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5713 {
5714 $writer = new \Kibo\Phast\Logging\LogWriters\Composite\Writer();
5715 $factory = new \Kibo\Phast\Logging\LogWriters\Factory();
5716 foreach ($config['logWriters'] as $writerConfig) {
5717 $writer->addWriter($factory->make($writerConfig, $request));
5718 }
5719 return $writer;
5720 }
5721 }
5722 namespace Kibo\Phast\Logging\LogWriters\RotatingTextFile;
5723
5724 class Factory
5725 {
5726 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5727 {
5728 return new \Kibo\Phast\Logging\LogWriters\RotatingTextFile\Writer($config);
5729 }
5730 }
5731 namespace Kibo\Phast\Logging\LogWriters\PHPError;
5732
5733 class Factory
5734 {
5735 public function make(array $config, \Kibo\Phast\Services\ServiceRequest $request)
5736 {
5737 return new \Kibo\Phast\Logging\LogWriters\PHPError\Writer($config);
5738 }
5739 }
5740 namespace Kibo\Phast\Logging\Common;
5741
5742 trait JSONLFileLogTrait
5743 {
5744 /**
5745 * @var string
5746 */
5747 private $dir;
5748 /**
5749 * @var string
5750 */
5751 private $filename;
5752 /**
5753 * JSONLFileLogWriter constructor.
5754 * @param string $dir
5755 * @param string $suffix
5756 */
5757 public function __construct($dir, $suffix)
5758 {
5759 $this->dir = $dir;
5760 $suffix = preg_replace('/[^0-9A-Za-z_-]/', '', (string) $suffix);
5761 if (!empty($suffix)) {
5762 $suffix = '-' . $suffix;
5763 }
5764 $this->filename = $this->dir . '/log' . $suffix . '.jsonl';
5765 }
5766 }
5767 namespace Kibo\Phast\Logging;
5768
5769 class LogEntry implements \JsonSerializable
5770 {
5771 /**
5772 * @var int
5773 */
5774 private $level;
5775 /**
5776 * @var string
5777 */
5778 private $message;
5779 /**
5780 * @var array
5781 */
5782 private $context;
5783 /**
5784 * LogEntry constructor.
5785 * @param int $level
5786 * @param string $message
5787 * @param array $context
5788 */
5789 public function __construct($level, $message, array $context)
5790 {
5791 $this->level = (int) $level;
5792 $this->message = $message;
5793 $this->context = $context;
5794 }
5795 /**
5796 * @return int
5797 */
5798 public function getLevel()
5799 {
5800 return $this->level;
5801 }
5802 /**
5803 * @return string
5804 */
5805 public function getMessage()
5806 {
5807 return $this->message;
5808 }
5809 /**
5810 * @return array
5811 */
5812 public function getContext()
5813 {
5814 return $this->context;
5815 }
5816 public function toArray()
5817 {
5818 return ['level' => $this->level, 'message' => $this->message, 'context' => $this->context];
5819 }
5820 public function jsonSerialize()
5821 {
5822 return $this->toArray();
5823 }
5824 }
5825 namespace Kibo\Phast\Logging;
5826
5827 class Logger
5828 {
5829 /**
5830 * @var LogWriter
5831 */
5832 private $writer;
5833 /**
5834 * @var array
5835 */
5836 private $context = array();
5837 /**
5838 * @var ObjectifiedFunctions
5839 */
5840 private $functions;
5841 /**
5842 * Logger constructor.
5843 * @param LogWriter $writer
5844 */
5845 public function __construct(\Kibo\Phast\Logging\LogWriter $writer, \Kibo\Phast\Common\ObjectifiedFunctions $functions = null)
5846 {
5847 $this->writer = $writer;
5848 $this->functions = is_null($functions) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $functions;
5849 }
5850 /**
5851 * Returns a new logger with default context
5852 * merged from the current logger and the passed array
5853 *
5854 * @param array $context
5855 * @return Logger
5856 */
5857 public function withContext(array $context)
5858 {
5859 $logger = clone $this;
5860 $logger->context = array_merge($this->context, $context);
5861 return $logger;
5862 }
5863 /**
5864 * System is unusable.
5865 *
5866 * @param string $message
5867 * @param array $context
5868 *
5869 * @return void
5870 */
5871 public function emergency($message, array $context = array())
5872 {
5873 $this->log(\Kibo\Phast\Logging\LogLevel::EMERGENCY, $message, $context);
5874 }
5875 /**
5876 * Action must be taken immediately.
5877 *
5878 * Example: Entire website down, database unavailable, etc. This should
5879 * trigger the SMS alerts and wake you up.
5880 *
5881 * @param string $message
5882 * @param array $context
5883 *
5884 * @return void
5885 */
5886 public function alert($message, array $context = array())
5887 {
5888 $this->log(\Kibo\Phast\Logging\LogLevel::ALERT, $message, $context);
5889 }
5890 /**
5891 * Critical conditions.
5892 *
5893 * Example: Application component unavailable, unexpected exception.
5894 *
5895 * @param string $message
5896 * @param array $context
5897 *
5898 * @return void
5899 */
5900 public function critical($message, array $context = array())
5901 {
5902 $this->log(\Kibo\Phast\Logging\LogLevel::CRITICAL, $message, $context);
5903 }
5904 /**
5905 * Runtime errors that do not require immediate action but should typically
5906 * be logged and monitored.
5907 *
5908 * @param string $message
5909 * @param array $context
5910 *
5911 * @return void
5912 */
5913 public function error($message, array $context = array())
5914 {
5915 $this->log(\Kibo\Phast\Logging\LogLevel::ERROR, $message, $context);
5916 }
5917 /**
5918 * Exceptional occurrences that are not errors.
5919 *
5920 * Example: Use of deprecated APIs, poor use of an API, undesirable things
5921 * that are not necessarily wrong.
5922 *
5923 * @param string $message
5924 * @param array $context
5925 *
5926 * @return void
5927 */
5928 public function warning($message, array $context = array())
5929 {
5930 $this->log(\Kibo\Phast\Logging\LogLevel::WARNING, $message, $context);
5931 }
5932 /**
5933 * Normal but significant events.
5934 *
5935 * @param string $message
5936 * @param array $context
5937 *
5938 * @return void
5939 */
5940 public function notice($message, array $context = array())
5941 {
5942 $this->log(\Kibo\Phast\Logging\LogLevel::NOTICE, $message, $context);
5943 }
5944 /**
5945 * Interesting events.
5946 *
5947 * Example: User logs in, SQL logs.
5948 *
5949 * @param string $message
5950 * @param array $context
5951 *
5952 * @return void
5953 */
5954 public function info($message, array $context = array())
5955 {
5956 $this->log(\Kibo\Phast\Logging\LogLevel::INFO, $message, $context);
5957 }
5958 /**
5959 * Detailed debug information.
5960 *
5961 * @param string $message
5962 * @param array $context
5963 *
5964 * @return void
5965 */
5966 public function debug($message, array $context = array())
5967 {
5968 $this->log(\Kibo\Phast\Logging\LogLevel::DEBUG, $message, $context);
5969 }
5970 protected function log($level, $message, array $context = array())
5971 {
5972 $context = array_merge(['timestamp' => $this->functions->microtime(true)], $context);
5973 $this->writer->writeEntry(new \Kibo\Phast\Logging\LogEntry($level, $message, array_merge($this->context, $context)));
5974 }
5975 }
5976 namespace Kibo\Phast\Logging;
5977
5978 interface LogReader
5979 {
5980 /**
5981 * Reads LogMessage objects
5982 *
5983 * @return \Generator
5984 */
5985 public function readEntries();
5986 }
5987 namespace Kibo\Phast\Logging;
5988
5989 trait LoggingTrait
5990 {
5991 protected function logger($method = null, $line = null)
5992 {
5993 $context = ['class' => get_class($this)];
5994 if (!is_null($method)) {
5995 $context['method'] = $method;
5996 }
5997 if (!is_null($line)) {
5998 $context['line'] = $line;
5999 }
6000 return \Kibo\Phast\Logging\Log::context($context);
6001 }
6002 }
6003 namespace Kibo\Phast\Logging\LogReaders\JSONLFile;
6004
6005 class Reader implements \Kibo\Phast\Logging\LogReader
6006 {
6007 use \Kibo\Phast\Logging\Common\JSONLFileLogTrait;
6008 public function readEntries()
6009 {
6010 $fp = @fopen($this->filename, 'r');
6011 while ($fp && ($row = @fgets($fp))) {
6012 $decoded = @json_decode($row, true);
6013 if (!$decoded) {
6014 continue;
6015 }
6016 (yield new \Kibo\Phast\Logging\LogEntry(@$decoded['level'], @$decoded['message'], @$decoded['context']));
6017 }
6018 @fclose($fp);
6019 @unlink($this->filename);
6020 }
6021 public function __destruct()
6022 {
6023 if (!($dir = @opendir($this->dir))) {
6024 return;
6025 }
6026 $tenMinutesAgo = time() - 600;
6027 while ($file = @readdir($dir)) {
6028 $filename = $this->dir . "/{$file}";
6029 if (preg_match('/\\.jsonl$/', $file) && @filectime($filename) < $tenMinutesAgo) {
6030 @unlink($filename);
6031 }
6032 }
6033 }
6034 }
6035 namespace Kibo\Phast\Logging;
6036
6037 interface LogWriter
6038 {
6039 /**
6040 * Set a bit-mask to filter entries that are actually written
6041 *
6042 * @param int $mask
6043 * @return void
6044 */
6045 public function setLevelMask($mask);
6046 /**
6047 * Write an entry to the log
6048 *
6049 * @param LogEntry $entry
6050 * @return void
6051 */
6052 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry);
6053 }
6054 namespace Kibo\Phast\Services;
6055
6056 interface ServiceFilter
6057 {
6058 /**
6059 * @param Resource $resource
6060 * @param array $request
6061 * @return Resource
6062 */
6063 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request);
6064 }
6065 namespace Kibo\Phast\Services;
6066
6067 class Factory
6068 {
6069 /**
6070 * @param string $service
6071 * @param array $config
6072 * @return BaseService
6073 * @throws ItemNotFoundException
6074 */
6075 public function make($service, array $config)
6076 {
6077 if (!preg_match('/^[a-z]+$/', $service)) {
6078 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('Bad service');
6079 }
6080 $class = __NAMESPACE__ . '\\' . ucfirst($service) . '\\Factory';
6081 if (class_exists($class)) {
6082 return (new $class())->make($config);
6083 }
6084 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('Unknown service');
6085 }
6086 }
6087 namespace Kibo\Phast\Services;
6088
6089 trait ServiceFactoryTrait
6090 {
6091 /**
6092 * @param array $config
6093 * @param $cacheNamespace
6094 * @return UniversalRetriever
6095 */
6096 public function makeUniversalCachingRetriever(array $config, $cacheNamespace)
6097 {
6098 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
6099 $retriever->addRetriever(new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']));
6100 $retriever->addRetriever(new \Kibo\Phast\Retrievers\CachingRetriever(new \Kibo\Phast\Cache\File\Cache($config['cache'], $cacheNamespace), (new \Kibo\Phast\Retrievers\RemoteRetrieverFactory())->make($config)));
6101 return $retriever;
6102 }
6103 public function makeCachingServiceFilter(array $config, \Kibo\Phast\Filters\Service\CompositeFilter $compositeFilter, $cacheNamespace)
6104 {
6105 return new \Kibo\Phast\Filters\Service\CachingServiceFilter(new \Kibo\Phast\Cache\File\Cache($config['cache'], $cacheNamespace), $compositeFilter, new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']));
6106 }
6107 }
6108 namespace Kibo\Phast\Services\Bundler;
6109
6110 class Service
6111 {
6112 use \Kibo\Phast\Logging\LoggingTrait;
6113 /**
6114 * @var ServiceSignature
6115 */
6116 private $signature;
6117 /**
6118 * @var Retriever
6119 */
6120 private $cssRetriever;
6121 /**
6122 * @var ServiceFilter
6123 */
6124 private $cssFilter;
6125 /**
6126 * @var Retriever
6127 */
6128 private $jsRetriever;
6129 /**
6130 * @var ServiceFilter
6131 */
6132 private $jsFilter;
6133 private $tokenRefMaker;
6134 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)
6135 {
6136 $this->signature = $signature;
6137 $this->cssRetriever = $cssRetriever;
6138 $this->cssFilter = $cssFilter;
6139 $this->jsRetriever = $jsRetriever;
6140 $this->jsFilter = $jsFilter;
6141 $this->tokenRefMaker = $tokenRefMaker;
6142 }
6143 /**
6144 * @param ServiceRequest $request
6145 * @return Response
6146 */
6147 public function serve(\Kibo\Phast\Services\ServiceRequest $request)
6148 {
6149 $response = new \Kibo\Phast\HTTP\Response();
6150 $response->setHeader('Content-Type', 'application/json');
6151 $response->setContent($this->streamResponse($request));
6152 return $response;
6153 }
6154 private function streamResponse(\Kibo\Phast\Services\ServiceRequest $request)
6155 {
6156 (yield '[');
6157 $firstRow = true;
6158 foreach ($this->getParams($request) as $key => $params) {
6159 if (isset($params['ref'])) {
6160 $ref = $params['ref'];
6161 $params = $this->tokenRefMaker->getParams($ref);
6162 if (!$params) {
6163 $this->logger()->error('Could not resolve ref {ref}', ['ref' => $ref]);
6164 (yield $this->generateJSONRow(['status' => 404], $firstRow));
6165 continue;
6166 }
6167 }
6168 if (!isset($params['src'])) {
6169 $this->logger()->error('No src found for set {key}', ['key' => $key]);
6170 (yield $this->generateJSONRow(['status' => 404], $firstRow));
6171 continue;
6172 }
6173 if (!$this->verifyParams($params)) {
6174 $this->logger()->error('Params verification failed for set {key}', ['key' => $key]);
6175 (yield $this->generateJSONRow(['status' => 401], $firstRow));
6176 continue;
6177 }
6178 list($retriever, $filter) = $this->getRetrieverAndFilter($params);
6179 $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever(\Kibo\Phast\ValueObjects\URL::fromString($params['src']), $retriever);
6180 try {
6181 $this->logger()->info('Applying for set {key}', ['key' => $key]);
6182 $filtered = $filter->apply($resource, $params);
6183 (yield $this->generateJSONRow(['status' => 200, 'content' => $filtered->getContent()], $firstRow));
6184 } catch (\Kibo\Phast\Exceptions\ItemNotFoundException $e) {
6185 $this->logger()->error('Could not find {url} for set {key}', ['url' => $params['src'], 'key' => $key]);
6186 (yield $this->generateJSONRow(['status' => 404], $firstRow));
6187 } catch (\Exception $e) {
6188 $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()]);
6189 (yield $this->generateJSONRow(['status' => 500], $firstRow));
6190 }
6191 }
6192 (yield ']');
6193 }
6194 private function getParams(\Kibo\Phast\Services\ServiceRequest $request)
6195 {
6196 $params = $request->getParams();
6197 if (isset($params['src_0'])) {
6198 return (new \Kibo\Phast\Services\Bundler\BundlerParamsParser())->parse($request);
6199 }
6200 return (new \Kibo\Phast\Services\Bundler\ShortBundlerParamsParser())->parse($request);
6201 }
6202 private function verifyParams(array $params)
6203 {
6204 return \Kibo\Phast\Services\Bundler\ServiceParams::fromArray($params)->verify($this->signature);
6205 }
6206 private function getRetrieverAndFilter(array $params)
6207 {
6208 if (isset($params['isScript'])) {
6209 return [$this->jsRetriever, $this->jsFilter];
6210 }
6211 return [$this->cssRetriever, $this->cssFilter];
6212 }
6213 private function generateJSONRow(array $content, &$firstRow)
6214 {
6215 if (!$firstRow) {
6216 $prepend = ',';
6217 } else {
6218 $prepend = '';
6219 $firstRow = false;
6220 }
6221 return $prepend . \Kibo\Phast\Common\JSON::encode($content);
6222 }
6223 }
6224 namespace Kibo\Phast\Services\Bundler;
6225
6226 class BundlerParamsParser
6227 {
6228 public function parse(\Kibo\Phast\Services\ServiceRequest $request)
6229 {
6230 $result = [];
6231 foreach ($request->getParams() as $name => $value) {
6232 if (strpos($name, '_') !== false) {
6233 list($name, $key) = explode('_', $name, 2);
6234 $result[$key][$name] = $value;
6235 }
6236 }
6237 return $result;
6238 }
6239 }
6240 namespace Kibo\Phast\Services\Bundler;
6241
6242 class ShortBundlerParamsParser
6243 {
6244 public static function getParamsMappings()
6245 {
6246 return ['s' => 'src', 'i' => 'strip-imports', 'c' => 'cacheMarker', 't' => 'token', 'j' => 'isScript', 'r' => 'ref'];
6247 }
6248 public function parse(\Kibo\Phast\Services\ServiceRequest $request)
6249 {
6250 $query_string = $request->getHTTPRequest()->getQueryString();
6251 if (preg_match('/(^|&)f=/', $query_string)) {
6252 $query = \Kibo\Phast\ValueObjects\Query::fromString($this->unobfuscateQuery($query_string));
6253 } else {
6254 $query = $request->getQuery();
6255 }
6256 $query = $this->unshortenParams($query->getIterator());
6257 $query = $this->uncompressSrcs($query);
6258 $result = [];
6259 $current = null;
6260 foreach ($query as $key => $value) {
6261 if (in_array($key, ['src', 'ref'])) {
6262 if ($current) {
6263 $result[] = $current;
6264 }
6265 $current = [];
6266 }
6267 if ($current !== null) {
6268 $current[$key] = $value;
6269 }
6270 }
6271 if ($current) {
6272 $result[] = $current;
6273 }
6274 return $result;
6275 }
6276 private function unobfuscateQuery($query)
6277 {
6278 $query = str_rot13($query);
6279 if (strpos($query, '%2S') !== false) {
6280 $query = preg_replace_callback('/%../', function ($match) {
6281 return str_rot13($match[0]);
6282 }, $query);
6283 }
6284 return $query;
6285 }
6286 private function unshortenParams(\Generator $query)
6287 {
6288 $mappings = self::getParamsMappings();
6289 foreach ($query as $key => $value) {
6290 if (isset($mappings[$key])) {
6291 (yield $mappings[$key] => $value === '' ? '1' : $value);
6292 } else {
6293 (yield $key => $value);
6294 }
6295 }
6296 }
6297 private function uncompressSrcs(\Generator $query)
6298 {
6299 $lastUrl = '';
6300 foreach ($query as $key => $value) {
6301 if ($key === 'src') {
6302 $prefixLength = (int) base_convert(substr($value, 0, 2), 36, 10);
6303 $suffix = substr($value, 2);
6304 $value = substr($lastUrl, 0, $prefixLength) . $suffix;
6305 $lastUrl = $value;
6306 }
6307 (yield $key => $value);
6308 }
6309 }
6310 }
6311 namespace Kibo\Phast\Services\Bundler;
6312
6313 class TokenRefMaker
6314 {
6315 private $cache;
6316 public function __construct(\Kibo\Phast\Cache\Cache $cache)
6317 {
6318 $this->cache = $cache;
6319 }
6320 public function getRef($token, array $params)
6321 {
6322 $ref = \Kibo\Phast\Common\Base64url::shortHash(\Kibo\Phast\Common\JSON::encode($params));
6323 $cachedParams = $this->cache->get($ref);
6324 if (!$cachedParams) {
6325 $this->cache->set($ref, $params);
6326 $cachedParams = $this->cache->get($ref);
6327 }
6328 if ($cachedParams === $params) {
6329 return $ref;
6330 }
6331 }
6332 public function getParams($ref)
6333 {
6334 return $this->cache->get($ref);
6335 }
6336 }
6337 namespace Kibo\Phast\Services\Bundler;
6338
6339 class TokenRefMakerFactory
6340 {
6341 public function make(array $config)
6342 {
6343 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'token-refs');
6344 return new \Kibo\Phast\Services\Bundler\TokenRefMaker($cache);
6345 }
6346 }
6347 namespace Kibo\Phast\Services\Bundler;
6348
6349 class ServiceParams
6350 {
6351 /**
6352 * @var string
6353 */
6354 private $token;
6355 /**
6356 * @var array
6357 */
6358 private $params;
6359 private function __construct()
6360 {
6361 }
6362 /**
6363 * @param array $params
6364 * @return ServiceParams
6365 */
6366 public static function fromArray(array $params)
6367 {
6368 $instance = new self();
6369 if (isset($params['token'])) {
6370 $instance->token = $params['token'];
6371 unset($params['token']);
6372 }
6373 $instance->params = $params;
6374 return $instance;
6375 }
6376 /**
6377 * @param ServiceSignature $signature
6378 * @return ServiceParams
6379 */
6380 public function sign(\Kibo\Phast\Security\ServiceSignature $signature)
6381 {
6382 $new = new self();
6383 $new->token = $this->makeToken($signature);
6384 $new->params = $this->params;
6385 return $new;
6386 }
6387 /**
6388 * @param ServiceSignature $signature
6389 * @return bool
6390 */
6391 public function verify(\Kibo\Phast\Security\ServiceSignature $signature)
6392 {
6393 if (!isset($this->token)) {
6394 return false;
6395 }
6396 return $this->token == $this->makeToken($signature);
6397 }
6398 /**
6399 * @return mixed
6400 */
6401 public function toArray()
6402 {
6403 $params = $this->params;
6404 if ($this->token) {
6405 $params['token'] = $this->token;
6406 }
6407 return $params;
6408 }
6409 public function serialize()
6410 {
6411 return \Kibo\Phast\Common\JSON::encode($this->toArray());
6412 }
6413 private function makeToken(\Kibo\Phast\Security\ServiceSignature $signature)
6414 {
6415 $params = $this->params;
6416 if (isset($params['cacheMarker'])) {
6417 unset($params['cacheMarker']);
6418 }
6419 ksort($params);
6420 array_walk($params, function (&$item) {
6421 $item = (string) $item;
6422 });
6423 return $signature->sign(json_encode($params));
6424 }
6425 public function replaceByTokenRef(\Kibo\Phast\Services\Bundler\TokenRefMaker $maker)
6426 {
6427 if (!isset($this->token)) {
6428 return $this;
6429 }
6430 $ref = $maker->getRef($this->token, $this->toArray());
6431 return $ref ? \Kibo\Phast\Services\Bundler\ServiceParams::fromArray(['ref' => $ref]) : $this;
6432 }
6433 }
6434 namespace Kibo\Phast\Services\Bundler;
6435
6436 class Factory
6437 {
6438 use \Kibo\Phast\Services\ServiceFactoryTrait;
6439 public function make(array $config)
6440 {
6441 $cssServiceFactory = new \Kibo\Phast\Services\Css\Factory();
6442 $jsServiceFactory = new \Kibo\Phast\Services\Scripts\Factory();
6443 $cssFilter = $this->makeCachingServiceFilter($config, $cssServiceFactory->makeFilter($config), 'bundler-css');
6444 $jsFilter = $this->makeCachingServiceFilter($config, $jsServiceFactory->makeFilter($config), 'bundler-js');
6445 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));
6446 }
6447 }
6448 namespace Kibo\Phast\Services\Css;
6449
6450 class Factory
6451 {
6452 use \Kibo\Phast\Services\ServiceFactoryTrait;
6453 public function make(array $config)
6454 {
6455 $cssComposite = $this->makeFilter($config);
6456 $composite = $this->makeCachingServiceFilter($config, $cssComposite, 'css-processing-2');
6457 return new \Kibo\Phast\Services\Css\Service((new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config), [], $this->makeRetriever($config), $composite, $config);
6458 }
6459 public function makeRetriever(array $config)
6460 {
6461 return $this->makeUniversalCachingRetriever($config, 'css');
6462 }
6463 public function makeFilter(array $config)
6464 {
6465 return (new \Kibo\Phast\Filters\CSS\Composite\Factory())->make($config);
6466 }
6467 }
6468 namespace Kibo\Phast\Services\Diagnostics;
6469
6470 class Factory
6471 {
6472 public function make(array $config)
6473 {
6474 $logRoot = null;
6475 foreach ($config['logging']['logWriters'] as $writerConfig) {
6476 if ($writerConfig['class'] == \Kibo\Phast\Logging\LogWriters\JSONLFile\Writer::class) {
6477 $logRoot = $writerConfig['logRoot'];
6478 break;
6479 }
6480 }
6481 return new \Kibo\Phast\Services\Diagnostics\Service($logRoot);
6482 }
6483 }
6484 namespace Kibo\Phast\Services\Diagnostics;
6485
6486 class Service
6487 {
6488 private $logRoot;
6489 public function __construct($logRoot)
6490 {
6491 $this->logRoot = $logRoot;
6492 }
6493 public function serve(\Kibo\Phast\Services\ServiceRequest $request)
6494 {
6495 $params = $request->getParams();
6496 if (isset($params['documentRequestId'])) {
6497 $items = $this->getRequestLog($params['documentRequestId']);
6498 } else {
6499 $items = $this->getSystemDiagnostics();
6500 }
6501 $response = new \Kibo\Phast\HTTP\Response();
6502 $response->setContent(\Kibo\Phast\Common\JSON::prettyEncode($items));
6503 $response->setHeader('Content-Type', 'application/json');
6504 return $response;
6505 }
6506 private function getRequestLog($requestId)
6507 {
6508 return iterator_to_array((new \Kibo\Phast\Logging\LogReaders\JSONLFile\Reader($this->logRoot, $requestId))->readEntries());
6509 }
6510 private function getSystemDiagnostics()
6511 {
6512 return (new \Kibo\Phast\Diagnostics\SystemDiagnostics())->run(require PHAST_CONFIG_FILE);
6513 }
6514 }
6515 namespace Kibo\Phast\Services\Scripts;
6516
6517 class Factory
6518 {
6519 use \Kibo\Phast\Services\ServiceFactoryTrait;
6520 public function make(array $config)
6521 {
6522 $cachedComposite = $this->makeFilter($config);
6523 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);
6524 }
6525 public function makeRetriever(array $config)
6526 {
6527 return $this->makeUniversalCachingRetriever($config, 'scripts');
6528 }
6529 public function makeFilter(array $config)
6530 {
6531 $filter = new \Kibo\Phast\Filters\Service\CompositeFilter();
6532 $filter->addFilter(new \Kibo\Phast\Filters\Text\Decode\Filter());
6533 $filter->addFilter(new \Kibo\Phast\Filters\JavaScript\Minification\JSMinifierFilter(@$config['scripts']['removeLicenseHeaders']));
6534 return $filter;
6535 }
6536 }
6537 namespace Kibo\Phast\Services\Images;
6538
6539 class Factory
6540 {
6541 public function make(array $config)
6542 {
6543 if ($config['images']['api-mode']) {
6544 $retriever = new \Kibo\Phast\Retrievers\PostDataRetriever();
6545 } else {
6546 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
6547 $retriever->addRetriever(new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']));
6548 $retriever->addRetriever((new \Kibo\Phast\Retrievers\RemoteRetrieverFactory())->make($config));
6549 }
6550 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);
6551 }
6552 }
6553 namespace Kibo\Phast\Services;
6554
6555 abstract class BaseService
6556 {
6557 use \Kibo\Phast\Logging\LoggingTrait;
6558 /**
6559 * @var ServiceSignature
6560 */
6561 protected $signature;
6562 /**
6563 * @var string[]
6564 */
6565 protected $whitelist = array();
6566 /**
6567 * @var Retriever
6568 */
6569 protected $retriever;
6570 /**
6571 * @var ServiceFilter
6572 */
6573 protected $filter;
6574 /**
6575 * @var array
6576 */
6577 protected $config;
6578 /**
6579 * BaseService constructor.
6580 * @param ServiceSignature $signature
6581 * @param array $whitelist
6582 * @param Retriever $retriever
6583 * @param ServiceFilter $filter
6584 * @param array $config
6585 */
6586 public function __construct(\Kibo\Phast\Security\ServiceSignature $signature, array $whitelist, \Kibo\Phast\Retrievers\Retriever $retriever, \Kibo\Phast\Services\ServiceFilter $filter, array $config)
6587 {
6588 $this->signature = $signature;
6589 $this->whitelist = $whitelist;
6590 $this->retriever = $retriever;
6591 $this->filter = $filter;
6592 $this->config = $config;
6593 }
6594 /**
6595 * @param ServiceRequest $request
6596 * @return Response
6597 */
6598 public function serve(\Kibo\Phast\Services\ServiceRequest $request)
6599 {
6600 $this->validateRequest($request);
6601 $request = $this->getParams($request);
6602 $resource = \Kibo\Phast\ValueObjects\Resource::makeWithRetriever(\Kibo\Phast\ValueObjects\URL::fromString(isset($request['src']) ? $request['src'] : ''), $this->retriever);
6603 $filtered = $this->filter->apply($resource, $request);
6604 return $this->makeResponse($filtered, $request);
6605 }
6606 /**
6607 * @param ServiceRequest $request
6608 * @return array
6609 */
6610 protected function getParams(\Kibo\Phast\Services\ServiceRequest $request)
6611 {
6612 return $request->getParams();
6613 }
6614 /**
6615 * @param Resource $resource
6616 * @param array $request
6617 * @return Response
6618 */
6619 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
6620 {
6621 $response = new \Kibo\Phast\HTTP\Response();
6622 $response->setContent($resource->getContent());
6623 return $response;
6624 }
6625 protected function validateRequest(\Kibo\Phast\Services\ServiceRequest $request)
6626 {
6627 $this->validateIntegrity($request);
6628 try {
6629 $this->validateToken($request);
6630 } catch (\Kibo\Phast\Exceptions\UnauthorizedException $e) {
6631 $this->validateWhitelisted($request);
6632 }
6633 }
6634 protected function validateIntegrity(\Kibo\Phast\Services\ServiceRequest $request)
6635 {
6636 $params = $request->getParams();
6637 if (!isset($params['src'])) {
6638 throw new \Kibo\Phast\Exceptions\ItemNotFoundException('No source is set!');
6639 }
6640 }
6641 protected function validateToken(\Kibo\Phast\Services\ServiceRequest $request)
6642 {
6643 if (!$request->verify($this->signature)) {
6644 throw new \Kibo\Phast\Exceptions\UnauthorizedException('Invalid token in request: ' . $request->serialize(\Kibo\Phast\Services\ServiceRequest::FORMAT_QUERY));
6645 }
6646 }
6647 protected function validateWhitelisted(\Kibo\Phast\Services\ServiceRequest $request)
6648 {
6649 $params = $request->getParams();
6650 foreach ($this->whitelist as $pattern) {
6651 if (preg_match($pattern, $params['src'])) {
6652 return;
6653 }
6654 }
6655 throw new \Kibo\Phast\Exceptions\UnauthorizedException('Not allowed url: ' . $params['src']);
6656 }
6657 }
6658 namespace Kibo\Phast\Services;
6659
6660 class ServiceRequest
6661 {
6662 const FORMAT_QUERY = 1;
6663 const FORMAT_PATH = 2;
6664 private static $defaultSerializationMode = self::FORMAT_PATH;
6665 /**
6666 * @var string
6667 */
6668 private static $propagatedSwitches = '';
6669 /**
6670 * @var Switches
6671 */
6672 private static $switches;
6673 /**
6674 * @var string
6675 */
6676 private static $documentRequestId;
6677 /**
6678 * @var Request
6679 */
6680 private $httpRequest;
6681 /**
6682 * @var URL
6683 */
6684 private $url;
6685 /**
6686 * @var Query
6687 */
6688 private $query;
6689 /**
6690 * @var string
6691 */
6692 private $token;
6693 /**
6694 * @var bool
6695 */
6696 private $trusted = false;
6697 public function __construct()
6698 {
6699 if (!isset(self::$switches)) {
6700 self::$switches = new \Kibo\Phast\Environment\Switches();
6701 }
6702 $this->query = new \Kibo\Phast\ValueObjects\Query();
6703 }
6704 public static function resetRequestState()
6705 {
6706 self::$defaultSerializationMode = self::FORMAT_PATH;
6707 self::$propagatedSwitches = '';
6708 self::$switches = null;
6709 self::$documentRequestId = null;
6710 }
6711 public static function setDefaultSerializationMode($mode)
6712 {
6713 self::$defaultSerializationMode = $mode;
6714 }
6715 public static function getDefaultSerializationMode()
6716 {
6717 return self::$defaultSerializationMode;
6718 }
6719 public static function fromHTTPRequest(\Kibo\Phast\HTTP\Request $request)
6720 {
6721 $instance = new self();
6722 self::$switches = new \Kibo\Phast\Environment\Switches();
6723 $instance->httpRequest = $request;
6724 if ($request->getCookie('phast')) {
6725 self::$switches = \Kibo\Phast\Environment\Switches::fromString($request->getCookie('phast'));
6726 }
6727 if ($service = self::getRewrittenService($request)) {
6728 $instance->query = \Kibo\Phast\ValueObjects\Query::fromAssoc(['service' => $service, 'src' => $request->getAbsoluteURI()]);
6729 $instance->trusted = true;
6730 } else {
6731 $query = $request->getQuery();
6732 if ($query->get('src')) {
6733 $query->set('src', preg_replace('~^hxxp(?=s?://)~', 'http', $query->get('src')));
6734 }
6735 $pathInfo = $request->getPathInfo();
6736 if ($pathParams = self::parseBase64PathInfo($pathInfo)) {
6737 $query->update($pathParams);
6738 } elseif ($pathInfo) {
6739 $query->update(self::parsePathInfo($pathInfo));
6740 }
6741 if ($token = $query->pop('token')) {
6742 $instance->token = $token;
6743 }
6744 $instance->query = $query;
6745 if ($query->get('phast')) {
6746 self::$propagatedSwitches = $query->get('phast');
6747 $paramsSwitches = \Kibo\Phast\Environment\Switches::fromString($query->get('phast'));
6748 self::$switches = self::$switches->merge($paramsSwitches);
6749 }
6750 if ($query->get('documentRequestId')) {
6751 self::$documentRequestId = $query->get('documentRequestId');
6752 } else {
6753 self::$documentRequestId = (string) mt_rand(0, 999999999);
6754 }
6755 }
6756 return $instance;
6757 }
6758 public static function getRewrittenService(\Kibo\Phast\HTTP\Request $request)
6759 {
6760 if ($service = $request->getEnvValue('REDIRECT_PHAST_SERVICE')) {
6761 return $service;
6762 }
6763 if ($service = $request->getEnvValue('PHAST_SERVICE')) {
6764 return $service;
6765 }
6766 return null;
6767 }
6768 public function hasRequestSwitchesSet()
6769 {
6770 return !empty(self::$propagatedSwitches);
6771 }
6772 /**
6773 * @return Switches
6774 */
6775 public function getSwitches()
6776 {
6777 return self::$switches;
6778 }
6779 /**
6780 * @return array
6781 */
6782 public function getParams()
6783 {
6784 return $this->query->toAssoc();
6785 }
6786 /**
6787 * @return Query
6788 */
6789 public function getQuery()
6790 {
6791 return $this->query;
6792 }
6793 /**
6794 * @return Request
6795 */
6796 public function getHTTPRequest()
6797 {
6798 return $this->httpRequest;
6799 }
6800 /**
6801 * @return string
6802 */
6803 public function getDocumentRequestId()
6804 {
6805 return self::$documentRequestId;
6806 }
6807 /**
6808 * @param array $params
6809 * @return ServiceRequest
6810 */
6811 public function withParams(array $params)
6812 {
6813 $result = clone $this;
6814 $result->query = \Kibo\Phast\ValueObjects\Query::fromAssoc($params);
6815 return $result;
6816 }
6817 /**
6818 * @param URL $url
6819 * @return ServiceRequest
6820 */
6821 public function withUrl(\Kibo\Phast\ValueObjects\URL $url)
6822 {
6823 $result = clone $this;
6824 $result->url = $url;
6825 return $result;
6826 }
6827 /**
6828 * @param ServiceSignature $signature
6829 * @return ServiceRequest
6830 */
6831 public function sign(\Kibo\Phast\Security\ServiceSignature $signature)
6832 {
6833 $token = $signature->sign($this->getVerificationString());
6834 $result = clone $this;
6835 $result->token = $token;
6836 return $result;
6837 }
6838 /**
6839 * @param ServiceSignature $signature
6840 * @return bool
6841 */
6842 public function verify(\Kibo\Phast\Security\ServiceSignature $signature)
6843 {
6844 return $this->trusted || $signature->verify($this->token, $this->getVerificationString()) || $signature->verify($this->token, $this->getVerificationStringWithoutStemSuffix());
6845 }
6846 private static function parsePathInfo($string)
6847 {
6848 $values = new \Kibo\Phast\ValueObjects\Query();
6849 $parts = explode('/', $string);
6850 foreach ($parts as $part) {
6851 if ($part === '') {
6852 continue;
6853 }
6854 $pair = explode('=', $part);
6855 if (isset($pair[1])) {
6856 $values->set($pair[0], self::decodeSingleValue($pair[1]));
6857 } elseif (preg_match('/^__p__(@[1-9][0-9]*x)?\\./', $pair[0], $match)) {
6858 if (!empty($match[1]) && $values->has('src')) {
6859 $values->set('src', self::appendStemSuffix($values->get('src'), $match[1]));
6860 }
6861 break;
6862 } else {
6863 $values->set('src', self::decodeSingleValue($pair[0]));
6864 }
6865 }
6866 return $values;
6867 }
6868 private static function decodeSingleValue($value)
6869 {
6870 return urldecode(str_replace('-', '%', $value));
6871 }
6872 private static function appendStemSuffix($src, $suffix)
6873 {
6874 $url = \Kibo\Phast\ValueObjects\URL::fromString($src);
6875 $path = preg_replace_callback('/\\.\\w+$/', function ($match) use($suffix) {
6876 return $suffix . $match[0];
6877 }, $url->getPath());
6878 return $url->withPath($path)->toString();
6879 }
6880 private static function parseBase64PathInfo($string)
6881 {
6882 if (!preg_match('~
6883 (?<query> (?:/[a-z0-9_-]+)+ )
6884 \\.q
6885 (?<retina> @[1-9][0-9]*x )?
6886 \\.[a-z]{2,4} $
6887 ~ix', $string, $match)) {
6888 return null;
6889 }
6890 $data = str_replace('/', '', $match['query']);
6891 $values = \Kibo\Phast\ValueObjects\Query::fromString(\Kibo\Phast\Common\Base64url::decode($data));
6892 if (!empty($match['retina'])) {
6893 $values->set('src', self::appendStemSuffix($values->get('src'), $match['retina']));
6894 }
6895 return $values;
6896 }
6897 /**
6898 * @param callable $paramsFilter
6899 * @return string
6900 */
6901 private function getVerificationString($paramsFilter = null)
6902 {
6903 $params = $this->getAllParams();
6904 if ($paramsFilter) {
6905 $params = $paramsFilter($params);
6906 }
6907 ksort($params);
6908 return http_build_query($params);
6909 }
6910 private function getVerificationStringWithoutStemSuffix()
6911 {
6912 return $this->getVerificationString(function ($params) {
6913 if (isset($params['src'])) {
6914 $params['src'] = $this->stripStemSuffix($params['src']);
6915 }
6916 return $params;
6917 });
6918 }
6919 private function stripStemSuffix($src)
6920 {
6921 $url = \Kibo\Phast\ValueObjects\URL::fromString($src);
6922 $path = preg_replace('/@[1-9][0-9]*x(?=\\.\\w+$)/', '', $url->getPath());
6923 return $url->withPath($path)->toString();
6924 }
6925 public function serialize($format = null)
6926 {
6927 $params = $this->getAllParams();
6928 if ($this->token) {
6929 $params['token'] = $this->token;
6930 }
6931 if (is_null($format)) {
6932 $format = self::$defaultSerializationMode;
6933 }
6934 if ($format == self::FORMAT_PATH) {
6935 return $this->serializeToPathFormat($params);
6936 }
6937 return $this->serializeToQueryFormat($params);
6938 }
6939 public function getAllParams()
6940 {
6941 $urlParams = [];
6942 if ($this->url) {
6943 parse_str($this->url->getQuery(), $urlParams);
6944 }
6945 $params = array_merge($urlParams, $this->query->toAssoc());
6946 if (!empty(self::$propagatedSwitches)) {
6947 $params['phast'] = self::$propagatedSwitches;
6948 }
6949 if (self::$switches->isOn(\Kibo\Phast\Environment\Switches::SWITCH_DIAGNOSTICS)) {
6950 $params['documentRequestId'] = self::$documentRequestId;
6951 }
6952 return $params;
6953 }
6954 private function serializeToQueryFormat(array $params)
6955 {
6956 $encoded = http_build_query($params);
6957 if (!isset($this->url)) {
6958 return $encoded;
6959 }
6960 $serialized = preg_replace('~\\?.*~', '', (string) $this->url);
6961 if (self::$defaultSerializationMode === self::FORMAT_PATH && !preg_match('~/$~', $serialized)) {
6962 $serialized .= '/' . $this->getDummyFilename($params);
6963 }
6964 return $serialized . '?' . $encoded;
6965 }
6966 /** @return string */
6967 private function serializeToPathFormat(array $params)
6968 {
6969 $query = \Kibo\Phast\Common\Base64url::encode(http_build_query($params));
6970 $path = '/' . $this->insertPathSeparators($query . '.q.' . $this->getDummyExtension($params));
6971 if (isset($this->url)) {
6972 return preg_replace(['~\\?.*~', '~/$~'], '', $this->url) . $path;
6973 }
6974 return $path;
6975 }
6976 private function insertPathSeparators($path)
6977 {
6978 return strrev(implode('/', str_split(strrev($path), 255)));
6979 }
6980 private function getDummyFilename(array $params)
6981 {
6982 return '__p__.' . $this->getDummyExtension($params);
6983 }
6984 private function getDummyExtension(array $params)
6985 {
6986 $default = 'js';
6987 if (empty($params['src'])) {
6988 return $default;
6989 }
6990 $url = \Kibo\Phast\ValueObjects\URL::fromString($params['src']);
6991 $ext = strtolower($url->getExtension());
6992 if (preg_match('/^(jpe?g|gif|png|js|css)$/', $ext)) {
6993 return $ext;
6994 }
6995 return $default;
6996 }
6997 }
6998 namespace Kibo\Phast\Security;
6999
7000 class ServiceSignature
7001 {
7002 const AUTO_TOKEN_SIZE = 128;
7003 const SIGNATURE_LENGTH = 16;
7004 /**
7005 * @var Cache
7006 */
7007 private $cache;
7008 /**
7009 * @var array
7010 */
7011 private $identities;
7012 /**
7013 * ServiceSignature constructor.
7014 *
7015 * @param Cache $cache
7016 */
7017 public function __construct(\Kibo\Phast\Cache\Cache $cache)
7018 {
7019 $this->cache = $cache;
7020 }
7021 /**
7022 * @param string|array $identities
7023 */
7024 public function setIdentities($identities)
7025 {
7026 if (is_string($identities)) {
7027 $this->identities = ['' => $identities];
7028 } else {
7029 $this->identities = $identities;
7030 }
7031 }
7032 /**
7033 * @return string
7034 */
7035 public function getCacheSalt()
7036 {
7037 $identities = $this->getIdentities();
7038 return md5(join('=>', array_merge(array_keys($identities), array_values($identities))));
7039 }
7040 public function sign($value)
7041 {
7042 $identities = $this->getIdentities();
7043 $users = array_keys($identities);
7044 list($user, $token) = [array_shift($users), array_shift($identities)];
7045 return $user . substr(md5($token . $value), 0, self::SIGNATURE_LENGTH);
7046 }
7047 public function verify($signature, $value)
7048 {
7049 $user = substr($signature, 0, -self::SIGNATURE_LENGTH);
7050 $identities = $this->getIdentities();
7051 if (!isset($identities[$user])) {
7052 return false;
7053 }
7054 $token = $identities[$user];
7055 $signer = new self($this->cache);
7056 $signer->setIdentities([$user => $token]);
7057 return $signature === $signer->sign($value);
7058 }
7059 public static function generateToken()
7060 {
7061 $token = '';
7062 for ($i = 0; $i < self::AUTO_TOKEN_SIZE; $i++) {
7063 $token .= chr(mt_rand(33, 126));
7064 }
7065 return $token;
7066 }
7067 private function getIdentities()
7068 {
7069 if (!isset($this->identities)) {
7070 $token = $this->cache->get('security-token', function () {
7071 return self::generateToken();
7072 });
7073 $this->identities = ['' => $token];
7074 }
7075 return $this->identities;
7076 }
7077 }
7078 namespace Kibo\Phast\Security;
7079
7080 class ServiceSignatureFactory
7081 {
7082 public function make(array $config)
7083 {
7084 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'signature');
7085 $signature = new \Kibo\Phast\Security\ServiceSignature($cache);
7086 if (isset($config['securityToken'])) {
7087 $signature->setIdentities($config['securityToken']);
7088 }
7089 return $signature;
7090 }
7091 }
7092 namespace Kibo\Phast\Exceptions;
7093
7094 class LogicException extends \LogicException
7095 {
7096 }
7097 namespace Kibo\Phast\Exceptions;
7098
7099 class RuntimeException extends \RuntimeException
7100 {
7101 }
7102 namespace Kibo\Phast\Exceptions;
7103
7104 class CachedExceptionException extends \Exception
7105 {
7106 }
7107 namespace Kibo\Phast\Exceptions;
7108
7109 class ItemNotFoundException extends \Exception
7110 {
7111 /**
7112 * @var URL
7113 */
7114 private $url;
7115 public function __construct($message = '', $code = 0, \Throwable $previous = null, \Kibo\Phast\ValueObjects\URL $failed = null)
7116 {
7117 parent::__construct($message, $code, $previous);
7118 $this->url = $failed;
7119 }
7120 /**
7121 * @return URL
7122 */
7123 public function getUrl()
7124 {
7125 return $this->url;
7126 }
7127 }
7128 namespace Kibo\Phast\Exceptions;
7129
7130 class UnauthorizedException extends \Exception
7131 {
7132 }
7133 namespace Kibo\Phast\Exceptions;
7134
7135 class UndefinedObjectifiedFunction extends \RuntimeException
7136 {
7137 }
7138 namespace Kibo\PhastPlugins\SDK;
7139
7140 /**
7141 * Provides commonly needed URLs
7142 *
7143 * Interface HostURLs
7144 * @see URL
7145 */
7146 interface HostURLs
7147 {
7148 /**
7149 * The URL at which static resource (JS, CSS, IMG)
7150 * optimizations reside
7151 *
7152 * @return URL
7153 */
7154 public function getServicesURL();
7155 /**
7156 * The full URL of the root of the current site
7157 *
7158 * @return URL
7159 */
7160 public function getSiteURL();
7161 /**
7162 * The CDN equivalent of a specified URL
7163 *
7164 * @return URL
7165 */
7166 public function getCDNURL(\Kibo\Phast\ValueObjects\URL $url);
7167 /**
7168 * URL of the admin page at which
7169 * the plugin's settings are located
7170 *
7171 * @return URL
7172 */
7173 public function getSettingsURL();
7174 /**
7175 * URL for admin panel AJAX communication
7176 *
7177 * @return URL
7178 */
7179 public function getAJAXEndPoint();
7180 /**
7181 * A URL to a publicly available image.
7182 *
7183 * @return URL
7184 */
7185 public function getTestImageURL();
7186 }
7187 namespace Kibo\PhastPlugins\SDK;
7188
7189 /**
7190 * Services container for the Phast Plugins Services SDK
7191 *
7192 * Class SDK
7193 */
7194 class ServiceSDK
7195 {
7196 /**
7197 * @var ServiceHost
7198 */
7199 protected $host;
7200 /**
7201 * @var EnvironmentIdentifier
7202 */
7203 private $environmentIdentifier;
7204 public function __construct(\Kibo\PhastPlugins\SDK\ServiceHost $host)
7205 {
7206 $this->host = $host;
7207 $this->environmentIdentifier = new \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier();
7208 }
7209 public function getServiceAPI()
7210 {
7211 return new \Kibo\PhastPlugins\SDK\APIs\Service($this->getServiceConfiguration());
7212 }
7213 public function getServiceConfiguration()
7214 {
7215 return new \Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration($this->getPHPFilesServiceConfigurationRepository(), $this->getEnvironmentIdentifier(), $this->getCacheRootManager(), [$this->host, 'onServiceConfigurationLoad']);
7216 }
7217 public function getCacheRootManager()
7218 {
7219 return new \Kibo\PhastPlugins\SDK\Caching\CacheRootManager($this->host->getCacheRootCandidatesProvider());
7220 }
7221 /**
7222 * Returns a default implementation of the
7223 * ServiceConfigurationRepository interface
7224 *
7225 * @see ServiceConfigurationRepository
7226 * @return PHPFilesServiceConfigurationRepository
7227 */
7228 public function getPHPFilesServiceConfigurationRepository()
7229 {
7230 return new \Kibo\PhastPlugins\SDK\Configuration\PHPFilesServiceConfigurationRepository($this->getCacheRootManager());
7231 }
7232 public function getEnvironmentIdentifier()
7233 {
7234 return $this->environmentIdentifier;
7235 }
7236 }
7237 namespace Kibo\PhastPlugins\SDK;
7238
7239 /**
7240 * Services container for the Phast Plugins SDK
7241 *
7242 * Class SDK
7243 */
7244 class SDK extends \Kibo\PhastPlugins\SDK\ServiceSDK
7245 {
7246 /**
7247 * @var PluginHost
7248 */
7249 protected $host;
7250 /**
7251 * SDK constructor.
7252 * @param PluginHost $host
7253 */
7254 public function __construct(\Kibo\PhastPlugins\SDK\PluginHost $host)
7255 {
7256 parent::__construct($host);
7257 }
7258 /**
7259 * The current SDK version
7260 *
7261 * @return string
7262 */
7263 public function getSDKVersion()
7264 {
7265 return '8';
7266 }
7267 /**
7268 * The current plugin version.
7269 * Composed from the host plugin name,
7270 * the host plugin version
7271 * and the SDK version
7272 *
7273 * @return string
7274 */
7275 public function getPluginVersion()
7276 {
7277 return join('-', [$this->host->getPluginHostName(), $this->host->getPluginHostVersion(), $this->getSDKVersion()]);
7278 }
7279 /**
7280 * @return Phast
7281 */
7282 public function getPhastAPI()
7283 {
7284 return new \Kibo\PhastPlugins\SDK\APIs\Phast($this->getPhastConfiguration());
7285 }
7286 public function getAdminPanel()
7287 {
7288 return new \Kibo\PhastPlugins\SDK\AdminPanel\AdminPanel($this->host->getPhastUser(), $this->host->getHostURLs()->getAJAXEndPoint(), $this->getAdminPanelData(), $this->getTranslationsManager(), $this->host->isDev());
7289 }
7290 public function getAJAXRequestsDispatcher()
7291 {
7292 return new \Kibo\PhastPlugins\SDK\AJAX\RequestsDispatcher($this->host->getPhastUser(), $this);
7293 }
7294 public function getAdminPanelData()
7295 {
7296 return new \Kibo\PhastPlugins\SDK\AdminPanel\AdminPanelData($this->getPluginConfiguration(), $this->getServiceConfigurationGenerator(), $this->getPhastConfiguration(), $this->getCacheRootManager(), $this->host);
7297 }
7298 public function getInstallNotice()
7299 {
7300 return new \Kibo\PhastPlugins\SDK\AdminPanel\InstallNotice($this->getPluginConfiguration(), $this->host->getInstallNoticeRenderer(), $this->getTranslationsManager(), $this->host->getHostURLs()->getSettingsURL(), $this->host->getHostURLs()->getAJAXEndPoint());
7301 }
7302 public function getPluginConfiguration()
7303 {
7304 return new \Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration($this->getPluginConfigurationRepository(), $this->getServiceConfigurationGenerator(), $this->getCacheRootManager(), $this->host->getPhastUser(), $this->host->getNonceChecker());
7305 }
7306 public function getPhastConfiguration()
7307 {
7308 return new \Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration($this->getServiceConfigurationGenerator(), $this->getServiceConfiguration(), $this->getPluginConfiguration(), [$this->host, 'onPhastConfigurationLoad']);
7309 }
7310 public function getAutoConfiguration()
7311 {
7312 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());
7313 }
7314 public function getTranslationsManager()
7315 {
7316 return new \Kibo\PhastPlugins\SDK\AdminPanel\TranslationsManager($this->host->getLocale(), $this->host->getPluginName());
7317 }
7318 private function getServiceConfigurationGenerator()
7319 {
7320 return new \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator($this->getPHPFilesServiceConfigurationRepository(), $this->getEnvironmentIdentifier(), $this->getPluginVersion(), $this->host->getHostURLs());
7321 }
7322 public function updatePreviewCookie($enable = true)
7323 {
7324 if (headers_sent()) {
7325 return false;
7326 }
7327 $enabled = isset($_COOKIE['PHAST_PREVIEW']) && (bool) $_COOKIE['PHAST_PREVIEW'];
7328 if (!$enabled && $enable) {
7329 return setcookie('PHAST_PREVIEW', '1', 0, '/');
7330 }
7331 if ($enabled && !$enable) {
7332 return setcookie('PHAST_PREVIEW', '0', 0, '/');
7333 }
7334 return true;
7335 }
7336 private function getPluginConfigurationRepository()
7337 {
7338 return new \Kibo\PhastPlugins\SDK\Configuration\PluginConfigurationRepository($this->host->getKeyValueStore());
7339 }
7340 }
7341 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7342
7343 /**
7344 * Represents the data that needs to be send
7345 * to the plugin's admin panel
7346 *
7347 * Class AdminPanelData
7348 */
7349 class AdminPanelData
7350 {
7351 /**
7352 * @var PluginConfiguration
7353 */
7354 private $pluginConfig;
7355 /**
7356 * @var ServiceConfigurationGenerator
7357 */
7358 private $serviceConfigGenerator;
7359 /**
7360 * @var PhastConfiguration
7361 */
7362 private $phastConfig;
7363 /**
7364 * @var CacheRootManager
7365 */
7366 private $cacheRootManager;
7367 /**
7368 * @var PluginHost
7369 */
7370 private $host;
7371 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)
7372 {
7373 $this->pluginConfig = $pluginConfig;
7374 $this->serviceConfigGenerator = $serviceConfigGenerator;
7375 $this->phastConfig = $phastConfig;
7376 $this->cacheRootManager = $cacheRootManager;
7377 $this->host = $host;
7378 }
7379 public function get()
7380 {
7381 $siteUrl = $this->host->getHostURLs()->getSiteURL();
7382 $urlWithPhast = $this->addQueryParam($siteUrl, 'phast', 'phast');
7383 $urlWithoutPhast = $this->addQueryParam($siteUrl, 'phast', '-phast');
7384 $pageSpeedToolUrl = 'https://developers.google.com/speed/pagespeed/insights/?url=';
7385 $errors = [];
7386 if (!$this->cacheRootManager->hasCacheRoot()) {
7387 $errors[] = ['type' => 'no-cache-root', 'params' => $this->cacheRootManager->getCacheRootCandidates()];
7388 }
7389 if (!$this->serviceConfigGenerator->generateIfNotExists($this->pluginConfig)) {
7390 $errors[] = ['type' => 'no-service-config', 'params' => $this->cacheRootManager->getCacheRootCandidates()];
7391 }
7392 $warnings = [];
7393 $api_client_warning = [];
7394 $phast_config = $this->phastConfig->get();
7395 $diagnostics = new \Kibo\Phast\Diagnostics\SystemDiagnostics();
7396 foreach ($diagnostics->run($phast_config) as $status) {
7397 if ($status->isAvailable()) {
7398 continue;
7399 }
7400 $package = $status->getPackage();
7401 $type = $package->getType();
7402 if ($type == 'Cache') {
7403 $errors[] = ['type' => 'cache', 'params' => [$status->getReason()]];
7404 } elseif ($type == 'ImageFilter') {
7405 $name = substr($package->getNamespace(), strrpos($package->getNamespace(), '\\') + 1);
7406 if ($name === 'ImageAPIClient') {
7407 $api_client_warning[] = 'Image optimization API error: ' . $status->getReason();
7408 } else {
7409 $warnings[] = $status->getReason();
7410 }
7411 }
7412 }
7413 $phastpress_config = $this->pluginConfig->get();
7414 if ($phastpress_config['img-optimization-api']) {
7415 $warnings = $api_client_warning;
7416 }
7417 $nonce = $this->host->getNonce();
7418 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()];
7419 }
7420 private function addQueryParam(\Kibo\Phast\ValueObjects\URL $url, $key, $value)
7421 {
7422 // TODO: Move this functionality to URL class
7423 $urlStr = (string) $url;
7424 $glue = strpos($urlStr, '?') === false ? '?' : '&';
7425 return $urlStr . $glue . $key . '=' . $value;
7426 }
7427 }
7428 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7429
7430 /**
7431 * Represents an installation notice
7432 * displayed everywhere in the host system's admin panel
7433 * upon plugin activation.
7434 *
7435 * Class InstallNotice
7436 */
7437 class InstallNotice
7438 {
7439 /**
7440 * @var PluginConfiguration
7441 */
7442 private $config;
7443 /**
7444 * @var InstallNoticeRenderer
7445 */
7446 private $renderer;
7447 /**
7448 * @var TranslationsManager
7449 */
7450 private $translations;
7451 /**
7452 * @var URL
7453 */
7454 private $settingsUrl;
7455 /**
7456 * @var URL
7457 */
7458 private $ajaxEntryPoint;
7459 /**
7460 * InstallNotice constructor.
7461 * @param PluginConfiguration $config
7462 * @param InstallNoticeRenderer $renderer
7463 * @param TranslationsManager $translations
7464 * @param URL $settingsUrl
7465 * @param URL $ajaxEndPoint
7466 */
7467 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)
7468 {
7469 $this->config = $config;
7470 $this->renderer = $renderer;
7471 $this->translations = $translations;
7472 $this->settingsUrl = $settingsUrl;
7473 $this->ajaxEntryPoint = $ajaxEndPoint;
7474 }
7475 /**
7476 * @return string The HTML to render the notice
7477 */
7478 public function render()
7479 {
7480 $display_message = $this->config->shouldShowActivationNotification();
7481 if (!$display_message) {
7482 return '';
7483 }
7484 $config = $this->config->get();
7485 if ($config['enabled'] && $config['admin-only']) {
7486 $status = 'Backend.status.admin';
7487 } elseif ($config['enabled']) {
7488 $status = 'Backend.status.on';
7489 } else {
7490 $status = 'Backend.status.off';
7491 }
7492 $message = $this->translations->get('Backend.install-notice', ['pluginState' => $this->translations->get($status), 'settingsUrl' => (string) $this->settingsUrl]);
7493 $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 ";
7494 return $this->renderer->render($message, $onCloseFunction);
7495 }
7496 }
7497 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7498
7499 interface InstallNoticeRenderer
7500 {
7501 /**
7502 * Renders a system notice.
7503 *
7504 * @param string $notice The message to show in the notice
7505 * @param string $onCloseJSFunction JavaScript function to call on the client when
7506 * an event that closes the notice occurs.
7507 * @return string HTML for the notice
7508 * @see InstallNotice
7509 */
7510 public function render($notice, $onCloseJSFunction);
7511 }
7512 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7513
7514 /**
7515 * Represents a nonce form field used XSS defense
7516 *
7517 * Class Nonce
7518 */
7519 class Nonce implements \JsonSerializable
7520 {
7521 /**
7522 * @var string
7523 */
7524 private $fieldName;
7525 /**
7526 * @var string
7527 */
7528 private $value;
7529 private function __construct()
7530 {
7531 }
7532 /**
7533 * @param string $fieldName The name of the field in the form
7534 * @param string $value The value of the field
7535 * @return Nonce
7536 */
7537 public static function make($fieldName, $value)
7538 {
7539 $instance = new self();
7540 $instance->fieldName = $fieldName;
7541 $instance->value = $value;
7542 return $instance;
7543 }
7544 /**
7545 * @return string
7546 */
7547 public function getFieldName()
7548 {
7549 return $this->fieldName;
7550 }
7551 /**
7552 * @return string
7553 */
7554 public function getValue()
7555 {
7556 return $this->value;
7557 }
7558 public function jsonSerialize()
7559 {
7560 return ['fieldName' => $this->fieldName, 'value' => $this->value];
7561 }
7562 }
7563 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7564
7565 /**
7566 * Represents the admin panel used for plugin configuration.
7567 * Use this class for rendering the admin panel of the plugin.
7568 *
7569 * Class AdminPanel
7570 */
7571 class AdminPanel
7572 {
7573 /**
7574 * @var PhastUser
7575 */
7576 private $user;
7577 /**
7578 * @var URL
7579 */
7580 private $ajaxEndPoint;
7581 /**
7582 * @var AdminPanelData
7583 */
7584 private $data;
7585 /**
7586 * @var TranslationsManager
7587 */
7588 private $translations;
7589 private $isDev = 'prod';
7590 private $scripts = array('prod' => array('main.js'), 'dev' => array('http://localhost:25903/main.js'));
7591 /**
7592 * AdminPanel constructor.
7593 * @param PhastUser $user
7594 * @param URL $ajaxEndPoint
7595 * @param AdminPanelData $data
7596 * @param TranslationsManager $translations
7597 * @param bool $isDev
7598 */
7599 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)
7600 {
7601 $this->user = $user;
7602 $this->ajaxEndPoint = $ajaxEndPoint;
7603 $this->data = $data;
7604 $this->translations = $translations;
7605 $this->isDev = (bool) $isDev;
7606 }
7607 /**
7608 * Returns the HTML needed to display the admin panel
7609 *
7610 * @return string
7611 */
7612 public function render()
7613 {
7614 if (!$this->user->mayModifySettings()) {
7615 return '';
7616 }
7617 $id = 'phast-plugins-sdk-admin-panel';
7618 $template = $this->getResourcesString();
7619 $template .= sprintf('
7620 <div id="%1$s"></div>
7621 <script>
7622 try {
7623 window.PHAST_PLUGINS_SDK_ADMIN_PANEL.apply(window, %2$s)
7624 } catch (e) {
7625 document.getElementById("%1$s").innerText = "Error: " + e.message
7626 throw e
7627 }
7628 </script>
7629 ', $id, json_encode([$id, $this->ajaxEndPoint->toString(), $this->data->get(), $this->translations->getAll()]));
7630 return $template;
7631 }
7632 private function getResourcesString()
7633 {
7634 return $this->isDev ? $this->getDevResourcesString() : $this->getProdResources();
7635 }
7636 private function getProdResources()
7637 {
7638 $resources = '';
7639 $base = __DIR__ . '/static/';
7640 foreach ($this->scripts['prod'] as $script) {
7641 $resources .= '<script>' . file_get_contents($base . $script) . '</script>';
7642 }
7643 return $resources;
7644 }
7645 private function getDevResourcesString()
7646 {
7647 $resources = '';
7648 foreach ($this->scripts['dev'] as $src) {
7649 $resources .= "<script src=\"{$src}\"></script>";
7650 }
7651 return $resources;
7652 }
7653 }
7654 namespace Kibo\PhastPlugins\SDK\AdminPanel;
7655
7656 class TranslationsManager
7657 {
7658 const DEFAULT_LOCALE = 'en';
7659 /**
7660 * @var string
7661 */
7662 private $locale;
7663 /**
7664 * @var string
7665 */
7666 private $pluginName;
7667 /**
7668 * @var string
7669 */
7670 private $languagesDir;
7671 /**
7672 * @var array
7673 */
7674 private $modules = array();
7675 public function __construct($locale, $pluginName)
7676 {
7677 $data = \Kibo\PhastPlugins\SDK\Generated\Translations::DATA;
7678 if (isset($data[$this->locale])) {
7679 $this->locale = $locale;
7680 } else {
7681 $this->locale = self::DEFAULT_LOCALE;
7682 }
7683 $this->pluginName = $pluginName;
7684 }
7685 public function getAll()
7686 {
7687 return array_merge(['plugin-name' => $this->pluginName], \Kibo\PhastPlugins\SDK\Generated\Translations::DATA[$this->locale]);
7688 }
7689 public function get($key, $interpolationArguments = array())
7690 {
7691 $keyParts = explode('.', $key);
7692 $transArr = \Kibo\PhastPlugins\SDK\Generated\Translations::DATA[$this->locale];
7693 while (count($keyParts) > 0) {
7694 $part = array_shift($keyParts);
7695 if (!isset($transArr[$part])) {
7696 return $key;
7697 }
7698 $transArr = $transArr[$part];
7699 }
7700 if (is_string($transArr)) {
7701 return $this->interpolate($transArr, $interpolationArguments);
7702 }
7703 return $key;
7704 }
7705 private function interpolate($string, $arguments)
7706 {
7707 $keys = array_map(function ($str) {
7708 return '{' . $str . '}';
7709 }, array_keys($arguments));
7710 $params = array_combine($keys, array_values($arguments));
7711 $params['@:plugin-name'] = $this->pluginName;
7712 return strtr($string, $params);
7713 }
7714 }
7715 namespace Kibo\PhastPlugins\SDK\Configuration;
7716
7717 /**
7718 * Represents the javascript used
7719 * for auto-configuration done
7720 * immediately after activation of the plugin.
7721 *
7722 * Class AutoConfiguration
7723 */
7724 class AutoConfiguration
7725 {
7726 /**
7727 * @var PluginConfiguration
7728 */
7729 private $pluginConfig;
7730 /**
7731 * @var PhastConfiguration
7732 */
7733 private $phastConfig;
7734 /**
7735 * @var URL
7736 */
7737 private $servicesUrl;
7738 /**
7739 * @var URL
7740 */
7741 private $testImageUrl;
7742 /**
7743 * @var Nonce
7744 */
7745 private $nonce;
7746 /**
7747 * @var URL
7748 */
7749 private $ajaxEndPoint;
7750 /**
7751 * AutoConfiguration constructor.
7752 * @param PluginConfiguration $pluginConfig
7753 * @param PhastConfiguration $phastConfig
7754 * @param URL $servicesUrl
7755 * @param URL $testImageUrl
7756 * @param Nonce $nonce
7757 * @param URL $ajaxEndPoint
7758 */
7759 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)
7760 {
7761 $this->pluginConfig = $pluginConfig;
7762 $this->phastConfig = $phastConfig;
7763 $this->servicesUrl = $servicesUrl;
7764 $this->testImageUrl = $testImageUrl;
7765 $this->nonce = $nonce;
7766 $this->ajaxEndPoint = $ajaxEndPoint;
7767 }
7768 /**
7769 * Returns the script that needs to rendered
7770 * in order for the script to get executed.
7771 *
7772 * @return string
7773 */
7774 public function renderScript()
7775 {
7776 if (!$this->pluginConfig->shouldAutoConfigure()) {
7777 return '';
7778 }
7779 $config = \Kibo\Phast\Environment\Configuration::fromDefaults()->withUserConfiguration(new \Kibo\Phast\Environment\Configuration($this->phastConfig->get()))->getRuntimeConfig()->toArray();
7780 $signature = (new \Kibo\Phast\Security\ServiceSignatureFactory())->make($config);
7781 $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);
7782 $nonce = json_encode($this->nonce);
7783 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>";
7784 }
7785 }
7786 namespace Kibo\PhastPlugins\SDK\Configuration;
7787
7788 class ServiceConfigurationGenerator
7789 {
7790 /**
7791 * @var ServiceConfigurationRepository
7792 */
7793 private $repository;
7794 /**
7795 * @var EnvironmentIdentifier
7796 */
7797 private $environmentIdentifier;
7798 /**
7799 * @var URL
7800 */
7801 private $servicesUrl;
7802 /**
7803 * @var URL
7804 */
7805 private $cdnServicesUrl;
7806 /**
7807 * @var string
7808 */
7809 private $pluginVersion;
7810 /**
7811 * @var string
7812 */
7813 private $cdnHost;
7814 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository $repository, \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier $environmentIdentifier, $pluginVersion, \Kibo\PhastPlugins\SDK\HostURLs $hostUrls)
7815 {
7816 $this->repository = $repository;
7817 $this->environmentIdentifier = $environmentIdentifier;
7818 $this->pluginVersion = $pluginVersion;
7819 $this->servicesUrl = $hostUrls->getServicesURL();
7820 $this->cdnServicesUrl = $hostUrls->getCDNURL($hostUrls->getServicesURL());
7821 $this->cdnHost = $hostUrls->getCDNURL($hostUrls->getSiteURL())->getHost();
7822 }
7823 public function generateIfNotExists(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig)
7824 {
7825 if (!$this->repository->has()) {
7826 return $this->generate($pluginConfig);
7827 }
7828 $config = $this->repository->get();
7829 $envId = $this->environmentIdentifier->getValue();
7830 if (empty($config['plugin_version']) || $config['plugin_version'] != $this->pluginVersion || empty($config['alternativeServicesUrls'][$envId]) || $config['alternativeServicesUrls'][$envId] != $this->getServicesURLString($pluginConfig)) {
7831 return $this->generate($pluginConfig);
7832 }
7833 return true;
7834 }
7835 public function generate(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig)
7836 {
7837 $previousConfig = $this->repository->get();
7838 $plugin_config = $pluginConfig->get();
7839 $plugin_version = $this->pluginVersion;
7840 $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];
7841 if (isset($previousConfig['alternativeServicesUrls'])) {
7842 $config['alternativeServicesUrls'] = $previousConfig['alternativeServicesUrls'];
7843 } else {
7844 $config['alternativeServicesUrls'] = [];
7845 }
7846 $id = $this->environmentIdentifier->getValue();
7847 unset($config['alternativeServicesUrls'][$id]);
7848 $config['alternativeServicesUrls'][$id] = $this->getServicesURLString($pluginConfig);
7849 $config['alternativeServicesUrls'] = array_slice($config['alternativeServicesUrls'], -1000);
7850 return $this->repository->store($config);
7851 }
7852 private function getServicesURLString(\Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig)
7853 {
7854 $plugin_config = $pluginConfig->get();
7855 if ($plugin_config['pathinfo-query-format']) {
7856 return (string) $this->cdnServicesUrl;
7857 }
7858 return (string) $this->servicesUrl;
7859 }
7860 }
7861 namespace Kibo\PhastPlugins\SDK\Configuration;
7862
7863 /**
7864 * Represents the configuration
7865 * that needs to be passed to be returned
7866 * by the callback passed to
7867 * \Kibo\Phast\PhastServices::serve()
7868 *
7869 * @see \Kibo\Phast\PhastServices::serve()
7870 * Class ServiceConfiguration
7871 */
7872 class ServiceConfiguration
7873 {
7874 /**
7875 * @var ServiceConfigurationRepository
7876 */
7877 private $repository;
7878 /**
7879 * @var EnvironmentIdentifier
7880 */
7881 private $environmentIdentifier;
7882 /**
7883 * @var CacheRootManager
7884 */
7885 private $cacheRootManager;
7886 /**
7887 * @var callable
7888 */
7889 private $onLoadCb;
7890 /**
7891 * ServiceConfiguration constructor.
7892 * @param ServiceConfigurationRepository $repository
7893 * @param CacheRootManager $cacheRootManager
7894 * @param callable $onLoadCb
7895 */
7896 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository $repository, \Kibo\PhastPlugins\SDK\Configuration\EnvironmentIdentifier $environmentIdentifier, \Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager, callable $onLoadCb)
7897 {
7898 $this->repository = $repository;
7899 $this->environmentIdentifier = $environmentIdentifier;
7900 $this->cacheRootManager = $cacheRootManager;
7901 $this->onLoadCb = $onLoadCb;
7902 }
7903 /**
7904 * Returns the configuration as config
7905 *
7906 * @return array|bool|mixed
7907 */
7908 public function get()
7909 {
7910 $config = $this->repository->get();
7911 $envId = $this->environmentIdentifier->getValue();
7912 if (isset($config['alternativeServicesUrls'][$envId])) {
7913 $config['servicesUrl'] = $config['alternativeServicesUrls'][$envId];
7914 }
7915 if (!empty($config['cdnHost'])) {
7916 $config['retrieverMap'][$config['cdnHost']] = \Kibo\Phast\HTTP\Request::fromGlobals()->getDocumentRoot();
7917 }
7918 $config['cache'] = ['cacheRoot' => $this->cacheRootManager->getCacheRoot()];
7919 $apiFilterName = \Kibo\Phast\Filters\Image\ImageAPIClient\Filter::class;
7920 $api_enabled = $config['images']['filters'][$apiFilterName]['enabled'];
7921 if (!$api_enabled) {
7922 unset($config['images']['filters'][$apiFilterName]);
7923 return call_user_func($this->onLoadCb, $config);
7924 }
7925 $config['images']['filters'][$apiFilterName]['host-name'] = $_SERVER['HTTP_HOST'];
7926 $config['images']['filters'][$apiFilterName]['request-uri'] = $_SERVER['REQUEST_URI'];
7927 $config['images']['filters'][$apiFilterName]['api-url'] = 'https://optimize.phast.io/?service=images';
7928 return call_user_func($this->onLoadCb, $config);
7929 }
7930 }
7931 namespace Kibo\PhastPlugins\SDK\Configuration;
7932
7933 class EnvironmentIdentifier
7934 {
7935 private $value;
7936 public function __construct()
7937 {
7938 $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']}");
7939 }
7940 public function getValue()
7941 {
7942 return $this->value;
7943 }
7944 }
7945 namespace Kibo\PhastPlugins\SDK\Configuration;
7946
7947 /**
7948 * Represents the configuration
7949 * that needs to be passed to
7950 * \Kibo\Phast\PhastDocumentFilters::deploy()
7951 * and
7952 * \Kibo\Phast\PhastDocumentFilters::apply()
7953 *
7954 * @see \Kibo\Phast\PhastDocumentFilters
7955 * Class PhastConfiguration
7956 */
7957 class PhastConfiguration
7958 {
7959 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));
7960 /**
7961 * @var ServiceConfigurationGenerator
7962 */
7963 private $serviceConfigGenerator;
7964 /**
7965 * @var ServiceConfiguration
7966 */
7967 private $serviceConfig;
7968 /**
7969 * @var PluginConfiguration
7970 */
7971 private $pluginConfig;
7972 /**
7973 * @var callable
7974 */
7975 private $onLoadCb;
7976 /**
7977 * PhastConfiguration constructor.
7978 * @param ServiceConfigurationGenerator $serviceConfigGenerator
7979 * @param ServiceConfiguration $serviceConfig
7980 * @param PluginConfiguration $pluginConfig
7981 * @param callable $onLoadCb
7982 */
7983 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationGenerator $serviceConfigGenerator, \Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration $serviceConfig, \Kibo\PhastPlugins\SDK\Configuration\PluginConfiguration $pluginConfig, callable $onLoadCb)
7984 {
7985 $this->serviceConfigGenerator = $serviceConfigGenerator;
7986 $this->serviceConfig = $serviceConfig;
7987 $this->pluginConfig = $pluginConfig;
7988 $this->onLoadCb = $onLoadCb;
7989 }
7990 /**
7991 * Returns the configuration to use on full html documents as an array
7992 *
7993 * @return array|bool|mixed
7994 */
7995 public function getForDocuments()
7996 {
7997 list($phastConfig, $pluginConfig) = $this->getPhastAndPluginConfigs();
7998 foreach (array_keys(self::SETTINGS_2_FILTERS) as $setting) {
7999 $this->setSettingInPhastConfig($setting, $pluginConfig, $phastConfig);
8000 }
8001 return call_user_func($this->onLoadCb, $phastConfig);
8002 }
8003 public function getForHTMLSnippets()
8004 {
8005 list($phastConfig, $pluginConfig) = $this->getPhastAndPluginConfigs();
8006 $defaultConfig = \Kibo\Phast\Environment\Configuration::fromDefaults()->toArray();
8007 $allFilters = array_keys($defaultConfig['documents']['filters']);
8008 foreach ($allFilters as $filter) {
8009 $phastConfig['documents']['filters'][$filter]['enabled'] = false;
8010 }
8011 $this->setSettingInPhastConfig('img-optimization-tags', $pluginConfig, $phastConfig);
8012 $this->setSettingInPhastConfig('img-optimization-css', $pluginConfig, $phastConfig);
8013 $this->setSettingInPhastConfig('img-lazy', $pluginConfig, $phastConfig);
8014 $phastConfig['optimizeHTMLDocumentsOnly'] = false;
8015 $phastConfig['outputServerSideStats'] = false;
8016 return call_user_func($this->onLoadCb, $phastConfig);
8017 }
8018 private function getPhastAndPluginConfigs()
8019 {
8020 // TODO: Optimize so we do not read from the service config file a bunch of times
8021 $this->serviceConfigGenerator->generateIfNotExists($this->pluginConfig);
8022 $pluginConfig = $this->pluginConfig->get();
8023 $phastConfig = $this->serviceConfig->get();
8024 $phastConfig['documents']['filters'] = [];
8025 $phastConfig['switches']['phast'] = $phastConfig && $this->pluginConfig->shouldDeployFilters();
8026 return [$phastConfig, $pluginConfig];
8027 }
8028 private function setSettingInPhastConfig($settingName, $pluginConfig, &$phastConfig)
8029 {
8030 foreach (self::SETTINGS_2_FILTERS[$settingName] as $filterClass) {
8031 if (!class_exists($filterClass)) {
8032 throw new \LogicException("No such filter: {$filterClass}");
8033 }
8034 if (strpos($filterClass, \Kibo\Phast\Filters\HTML::class . '\\') === 0) {
8035 $object = 'documents';
8036 } elseif (strpos($filterClass, \Kibo\Phast\Filters\CSS::class . '\\') === 0) {
8037 $object = 'styles';
8038 } else {
8039 throw new \LogicException("Invalid filter namespace: {$filterClass}");
8040 }
8041 $phastConfig[$object]['filters'][$filterClass] = ['enabled' => $settingName];
8042 $phastConfig['switches'][$settingName] = $pluginConfig[$settingName];
8043 }
8044 }
8045 /**
8046 * Returns the configuration as an array
8047 *
8048 * @return array|bool|mixed
8049 * @deprecated use PhastConfiguration::getForDocuments()
8050 */
8051 public function get()
8052 {
8053 return $this->getForDocuments();
8054 }
8055 }
8056 namespace Kibo\PhastPlugins\SDK\Configuration;
8057
8058 /**
8059 * Manages serialization of the phast service configuration.
8060 * Must be as fast as possible, having as little
8061 * dependency on the host system as possible (ideally - none).
8062 *
8063 * Interface ServiceConfigurationRepository
8064 */
8065 interface ServiceConfigurationRepository
8066 {
8067 /**
8068 * Store the given config
8069 *
8070 * @param array $config
8071 * @return bool TRUE on success, FALSE on failure
8072 */
8073 public function store(array $config);
8074 /**
8075 * Returns the previously stored config
8076 *
8077 * @return array|bool - The config on success or
8078 * FALSE on failure or if no config has been stored
8079 */
8080 public function get();
8081 /**
8082 * Tells whether a config has been previously stored
8083 *
8084 * @return bool
8085 */
8086 public function has();
8087 }
8088 namespace Kibo\PhastPlugins\SDK\Configuration;
8089
8090 class PluginConfigurationRepository
8091 {
8092 /**
8093 * @var KeyValueStore
8094 */
8095 private $store;
8096 /**
8097 * JSONKeyValueStore constructor.
8098 * @param KeyValueStore $store
8099 */
8100 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\KeyValueStore $store)
8101 {
8102 $this->store = $store;
8103 }
8104 /**
8105 * @param mixed $key
8106 * @param null $default
8107 * @return mixed|null
8108 */
8109 public function get($key, $default = null)
8110 {
8111 $value = $this->store->get($key);
8112 if (!is_string($value) || $value === 'null') {
8113 return $default;
8114 }
8115 $deserialised = @json_decode($value, true);
8116 if (is_null($deserialised)) {
8117 return $default;
8118 }
8119 return $deserialised;
8120 }
8121 /**
8122 * @param mixed $key
8123 * @param mixed $value
8124 */
8125 public function set($key, $value)
8126 {
8127 $this->store->set($key, json_encode($value));
8128 }
8129 }
8130 namespace Kibo\PhastPlugins\SDK\Configuration;
8131
8132 /**
8133 * A key-value store for use within the plugin's admin panel
8134 *
8135 * Interface KeyValueStore
8136 */
8137 interface KeyValueStore
8138 {
8139 /**
8140 * @param string $key
8141 * @return string|null The previously stored value or
8142 * null if there was no value stored for this key
8143 */
8144 public function get($key);
8145 /**
8146 * @param string $key
8147 * @param string $value
8148 * @return void
8149 */
8150 public function set($key, $value);
8151 }
8152 namespace Kibo\PhastPlugins\SDK\Configuration;
8153
8154 /**
8155 * Represents the configuration of the plugin
8156 *
8157 * Class PluginConfiguration
8158 */
8159 class PluginConfiguration
8160 {
8161 const KEY_SETTINGS = 'settings';
8162 const KEY_ACTIVATION_NOTIFICATION = 'activation-notification';
8163 /**
8164 * @var PluginConfigurationRepository
8165 */
8166 private $repo;
8167 /**
8168 * @var ServiceConfigurationGenerator
8169 */
8170 private $serviceConfigGenerator;
8171 /**
8172 * @var CacheRootManager
8173 */
8174 private $cacheRootManager;
8175 /**
8176 * @var PhastUser
8177 */
8178 private $user;
8179 /**
8180 * @var NonceChecker
8181 */
8182 private $nonceChecker;
8183 /**
8184 * PluginConfiguration constructor.
8185 * @param PluginConfigurationRepository $repo
8186 * @param ServiceConfigurationGenerator $serviceConfigGenerator
8187 * @param CacheRootManager $cacheRootManager
8188 * @param PhastUser $user
8189 * @param NonceChecker $nonceChecker
8190 */
8191 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)
8192 {
8193 $this->repo = $repo;
8194 $this->serviceConfigGenerator = $serviceConfigGenerator;
8195 $this->cacheRootManager = $cacheRootManager;
8196 $this->user = $user;
8197 $this->nonceChecker = $nonceChecker;
8198 }
8199 public function get()
8200 {
8201 $userSettings = $this->repo->get(self::KEY_SETTINGS, []);
8202 return array_merge($this->getDefaultAdminPanelSettings(), $userSettings);
8203 }
8204 public function save(array $newConfig)
8205 {
8206 if (!$this->nonceChecker->checkNonce($newConfig)) {
8207 return;
8208 }
8209 $keys = array_keys($this->getDefaultAdminPanelSettings());
8210 $settings = [];
8211 foreach ($keys as $key) {
8212 $newConfigKey = "phastpress-{$key}";
8213 if (!isset($newConfig[$newConfigKey])) {
8214 continue;
8215 }
8216 if ($newConfig[$newConfigKey] == 'on') {
8217 $settings[$key] = true;
8218 } elseif ($newConfig[$newConfigKey] == 'off') {
8219 $settings[$key] = false;
8220 }
8221 }
8222 $this->update($settings);
8223 }
8224 public function update(array $settings)
8225 {
8226 $this->repo->set(self::KEY_SETTINGS, array_merge($this->get(), $settings));
8227 $this->serviceConfigGenerator->generate($this);
8228 }
8229 public function shouldShowActivationNotification()
8230 {
8231 return $this->repo->get(self::KEY_ACTIVATION_NOTIFICATION, true);
8232 }
8233 public function hideActivationNotification()
8234 {
8235 $this->repo->set(self::KEY_ACTIVATION_NOTIFICATION, false);
8236 }
8237 public function shouldAutoConfigure()
8238 {
8239 return !$this->repo->get(self::KEY_SETTINGS);
8240 }
8241 public function shouldDeployFilters()
8242 {
8243 $plugin_config = $this->get();
8244 if (!$plugin_config['enabled']) {
8245 return false;
8246 }
8247 if (!$plugin_config['admin-only']) {
8248 return true;
8249 }
8250 return $this->user->seesPreviewMode();
8251 }
8252 public function shouldDisplayFooter()
8253 {
8254 return $this->get()['footer-link'] && $this->shouldDeployFilters();
8255 }
8256 private function getDefaultAdminPanelSettings()
8257 {
8258 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];
8259 }
8260 }
8261 namespace Kibo\PhastPlugins\SDK\Configuration;
8262
8263 /**
8264 * A default implementation of the ServiceConfigurationRepository interface
8265 *
8266 * Class PHPFilesServiceConfigurationRepository
8267 */
8268 class PHPFilesServiceConfigurationRepository implements \Kibo\PhastPlugins\SDK\Configuration\ServiceConfigurationRepository
8269 {
8270 /**
8271 * @var CacheRootManager
8272 */
8273 private $cacheRootManager;
8274 /**
8275 * PHPFilesServiceConfigurationRepository constructor.
8276 * @param CacheRootManager $cacheRootManager
8277 */
8278 public function __construct(\Kibo\PhastPlugins\SDK\Caching\CacheRootManager $cacheRootManager)
8279 {
8280 $this->cacheRootManager = $cacheRootManager;
8281 }
8282 public function store(array $config)
8283 {
8284 return $this->storeInPHPFile($this->getServiceConfigurationFilePath(), $config) !== false;
8285 }
8286 public function get()
8287 {
8288 $json = $this->readFromPHPFile($this->getServiceConfigurationFilePath());
8289 if (!$json) {
8290 return false;
8291 }
8292 if (strpos($json, 'a:') === 0) {
8293 $config = unserialize($json);
8294 } else {
8295 $config = json_decode($json, true);
8296 }
8297 if ($config === null) {
8298 return false;
8299 }
8300 return $config;
8301 }
8302 public function has()
8303 {
8304 return !!$this->get();
8305 }
8306 private function getServiceConfigurationFilePath()
8307 {
8308 return $this->getCacheStoredFilePath('service-config');
8309 }
8310 private function getCacheStoredFilePath($filename)
8311 {
8312 $dir = $this->cacheRootManager->getCacheRoot();
8313 if (!$dir) {
8314 return false;
8315 }
8316 $legacyName = "{$dir}/{$filename}.php";
8317 if (@file_exists($legacyName)) {
8318 return $legacyName;
8319 }
8320 foreach (@scandir($dir) as $file) {
8321 if (!preg_match('~^' . preg_quote($filename, '~') . '-[a-zA-Z0-9]{16}$~', $file)) {
8322 continue;
8323 }
8324 $path = "{$dir}/{$file}";
8325 if (@is_file($path)) {
8326 return $path;
8327 }
8328 }
8329 return "{$dir}/service-config-{$this->generateRandomName()}";
8330 }
8331 private function generateRandomName()
8332 {
8333 $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
8334 $o = '';
8335 for ($i = 0; $i < 16; $i++) {
8336 $o .= $chars[mt_rand(0, strlen($chars) - 1)];
8337 }
8338 return $o;
8339 }
8340 private function readFromPHPFile($filename)
8341 {
8342 $content = @file_get_contents($filename);
8343 if (!$content) {
8344 return false;
8345 }
8346 if (!preg_match('/^[^>]*>\\n([a-f0-9]{40})\\n(.*)$/s', $content, $match)) {
8347 return false;
8348 }
8349 if (sha1($match[2]) != $match[1]) {
8350 return false;
8351 }
8352 return $match[2];
8353 }
8354 private function storeInPHPFile($filename, $value)
8355 {
8356 $value = json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_UNESCAPED_SLASHES) . "\n";
8357 $content = "<?php exit; ?>\n" . sha1($value) . "\n" . $value;
8358 return @file_put_contents($filename, $content, LOCK_EX);
8359 }
8360 }
8361 namespace Kibo\PhastPlugins\SDK\APIs;
8362
8363 /**
8364 * Presents convenient methods for common tasks.
8365 *
8366 * Class Service
8367 */
8368 class Service
8369 {
8370 /**
8371 * @var ServiceConfiguration
8372 */
8373 private $config;
8374 /**
8375 * Service constructor.
8376 * @param ServiceConfiguration $config
8377 */
8378 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\ServiceConfiguration $config)
8379 {
8380 $this->config = $config;
8381 }
8382 /**
8383 * Configures the services and serves the request
8384 */
8385 public function serve()
8386 {
8387 \Kibo\Phast\PhastServices::serve(function () {
8388 return $this->config->get();
8389 });
8390 }
8391 }
8392 namespace Kibo\PhastPlugins\SDK\APIs;
8393
8394 /**
8395 * Presents convenient methods for common tasks.
8396 *
8397 * Class Phast
8398 */
8399 class Phast
8400 {
8401 /**
8402 * @var PhastConfiguration
8403 */
8404 private $config;
8405 /**
8406 * PhastAPI constructor.
8407 * @param PhastConfiguration $config
8408 */
8409 public function __construct(\Kibo\PhastPlugins\SDK\Configuration\PhastConfiguration $config)
8410 {
8411 $this->config = $config;
8412 }
8413 /**
8414 * Applies phast filters to $html
8415 * with a configuration suited for full documents
8416 *
8417 * @param $html
8418 * @return string
8419 */
8420 public function applyFiltersForDocument($html)
8421 {
8422 return \Kibo\Phast\PhastDocumentFilters::apply($html, $this->config->getForDocuments());
8423 }
8424 /**
8425 * Applies phast filters to $html
8426 * with a configuration suited for html snippets
8427 *
8428 * @param $html
8429 * @return string
8430 */
8431 public function applyFiltersForSnippets($html)
8432 {
8433 return \Kibo\Phast\PhastDocumentFilters::apply($html, $this->config->getForHTMLSnippets());
8434 }
8435 /**
8436 * Deploys phast output buffer filters
8437 * with a configuration suited for full documents
8438 */
8439 public function deployOutputBufferForDocument()
8440 {
8441 return \Kibo\Phast\PhastDocumentFilters::deploy($this->config->getForDocuments());
8442 }
8443 /**
8444 * Deploys phast output buffer filters
8445 * with a configuration suited for html snippets
8446 */
8447 public function deployOutputBufferForSnippets()
8448 {
8449 return \Kibo\Phast\PhastDocumentFilters::deploy($this->config->getForHTMLSnippets());
8450 }
8451 }
8452 namespace Kibo\PhastPlugins\SDK\Caching;
8453
8454 interface CacheRootCandidatesProvider
8455 {
8456 /**
8457 * Return a list of folders that will potentially be used
8458 * for storing cache and service configuration files.
8459 * The directories will be checked for write access
8460 * in the order they were provided. The first one writable
8461 * will be used.
8462 *
8463 * @return string[]
8464 */
8465 public function getCacheRootCandidates();
8466 }
8467 namespace Kibo\PhastPlugins\SDK\Caching;
8468
8469 class CacheRootManager
8470 {
8471 /**
8472 * @var CacheRootCandidatesProvider
8473 */
8474 private $rootsProvider;
8475 /**
8476 * CacheRootManager constructor.
8477 * @param CacheRootCandidatesProvider $rootsProvider
8478 */
8479 public function __construct(\Kibo\PhastPlugins\SDK\Caching\CacheRootCandidatesProvider $rootsProvider)
8480 {
8481 $this->rootsProvider = $rootsProvider;
8482 }
8483 public function getCacheRootCandidates()
8484 {
8485 return $this->rootsProvider->getCacheRootCandidates();
8486 }
8487 public function getCacheRoot()
8488 {
8489 $key = $this->getKey();
8490 $candidates = $this->getCacheRootCandidates();
8491 if ($result = $this->findExistingCacheRoot($key, $candidates)) {
8492 return $result;
8493 }
8494 if ($this->createNewCacheRoot($key, $candidates)) {
8495 return $this->findExistingCacheRoot($key, $candidates);
8496 }
8497 return false;
8498 }
8499 public function hasCacheRoot()
8500 {
8501 return (bool) $this->getCacheRoot();
8502 }
8503 public function getAllCacheRoots()
8504 {
8505 return $this->findAllExistingCacheRoots($this->getKey(), $this->getCacheRootCandidates());
8506 }
8507 private function getKey()
8508 {
8509 return md5(@$_SERVER['DOCUMENT_ROOT']) . '.' . (new \Kibo\Phast\Common\System())->getUserId();
8510 }
8511 private function findExistingCacheRoot($key, $candidates)
8512 {
8513 foreach ($this->findAllExistingCacheRoots($key, $candidates) as $checkDir) {
8514 if (!is_writable($checkDir)) {
8515 continue;
8516 }
8517 if (function_exists('posix_geteuid') && fileowner($checkDir) !== posix_geteuid()) {
8518 continue;
8519 }
8520 $this->createIndexFile($checkDir);
8521 return $checkDir;
8522 }
8523 return false;
8524 }
8525 private function findAllExistingCacheRoots($key, $candidates)
8526 {
8527 foreach ($this->getCacheRootCandidates() as $dir) {
8528 $checkDirs = ["{$dir}/{$key}", "{$dir}/phastpress.{$key}", "{$dir}/phast.{$key}"];
8529 foreach ($checkDirs as $checkDir) {
8530 if (!is_dir($checkDir)) {
8531 continue;
8532 }
8533 (yield $checkDir);
8534 }
8535 }
8536 }
8537 private function createNewCacheRoot($key, $candidates)
8538 {
8539 foreach ($this->getCacheRootCandidates() as $dir) {
8540 if (@mkdir("{$dir}/phast.{$key}", 0777, true)) {
8541 return true;
8542 }
8543 }
8544 return false;
8545 }
8546 private function createIndexFile($dir)
8547 {
8548 $path = "{$dir}/index.html";
8549 if (!@file_exists($path)) {
8550 @touch($path);
8551 }
8552 }
8553 }
8554 namespace Kibo\PhastPlugins\SDK;
8555
8556 class Autoloader
8557 {
8558 private static $instance;
8559 private $psr4 = array();
8560 public static function getInstance()
8561 {
8562 if (!isset(self::$instance)) {
8563 self::$instance = new self();
8564 self::$instance->install();
8565 }
8566 return self::$instance;
8567 }
8568 public function install()
8569 {
8570 spl_autoload_register(function ($class) {
8571 $this->autoload($class);
8572 });
8573 }
8574 public function addPSR4($namespace, $dir)
8575 {
8576 $this->psr4[] = [$namespace, $dir];
8577 return $this;
8578 }
8579 private function autoload($class)
8580 {
8581 foreach ($this->psr4 as $psr4) {
8582 list($namespace, $dir) = $psr4;
8583 if (strcasecmp($namespace . '\\', substr($class, 0, strlen($namespace) + 1))) {
8584 continue;
8585 }
8586 $relativeName = substr($class, strlen($namespace) + 1);
8587 $relativePath = str_replace('\\', '/', $relativeName) . '.php';
8588 $fullPath = $dir . '/' . $relativePath;
8589 if (file_exists($fullPath)) {
8590 include $fullPath;
8591 return;
8592 }
8593 }
8594 }
8595 }
8596 namespace Kibo\PhastPlugins\SDK;
8597
8598 interface ServiceHost
8599 {
8600 /**
8601 * @return CacheRootCandidatesProvider
8602 */
8603 public function getCacheRootCandidatesProvider();
8604 /**
8605 * Called right after the service configuration
8606 * has been loaded. Use it to modify the config
8607 * and take any other needed action before
8608 * the service is started.
8609 *
8610 * @param array $config - The configuration that has been loaded
8611 * @return array - The configuration to use for the services
8612 */
8613 public function onServiceConfigurationLoad(array $config);
8614 }
8615 namespace Kibo\PhastPlugins\SDK\Generated;
8616
8617 class Translations
8618 {
8619 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!
8620 ')), '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>.
8621 ', '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.'))))));
8622 }
8623 namespace Kibo\PhastPlugins\SDK\AJAX;
8624
8625 /**
8626 * Handles AJAX requests to the plugin admin.
8627 * Use this class to handle requests to the plugin's admin AJAX end point
8628 *
8629 * Class RequestsDispatcher
8630 */
8631 class RequestsDispatcher
8632 {
8633 const KEY_ACTION = 'phast-plugins-action';
8634 /**
8635 * @var PhastUser
8636 */
8637 private $user;
8638 /**
8639 * @var SDK
8640 */
8641 private $sdk;
8642 /**
8643 * RequestsDispatcher constructor.
8644 * @param PhastUser $user
8645 * @param SDK $sdk
8646 */
8647 public function __construct(\Kibo\PhastPlugins\SDK\Security\PhastUser $user, \Kibo\PhastPlugins\SDK\SDK $sdk)
8648 {
8649 $this->user = $user;
8650 $this->sdk = $sdk;
8651 }
8652 /**
8653 * Handles ajax requests to plugin's admin AJAX end point
8654 *
8655 * @param array $request The $_POST data to the request
8656 * @return mixed Must be json encoded and returned to the client
8657 */
8658 public function dispatch(array $request)
8659 {
8660 if (!$this->user->mayModifySettings()) {
8661 return false;
8662 }
8663 $action = isset($request[self::KEY_ACTION]) ? $request[self::KEY_ACTION] : '';
8664 if ($action == 'save-settings') {
8665 $this->sdk->getPluginConfiguration()->save($request);
8666 return $this->makeResponse(true, $this->sdk->getAdminPanelData()->get());
8667 }
8668 if ($action == 'dismiss-notice') {
8669 $this->sdk->getPluginConfiguration()->hideActivationNotification();
8670 return $this->makeResponse(true);
8671 }
8672 return $this->makeResponse(false);
8673 }
8674 private function makeResponse($success, $data = null)
8675 {
8676 return ['phast-success' => $success, 'phast-data' => $data];
8677 }
8678 }
8679 namespace Kibo\PhastPlugins\SDK\Security;
8680
8681 /**
8682 * Represents the user currently viewing the website (either backend or frontend)
8683 *
8684 * Interface PhastUser
8685 */
8686 interface PhastUser
8687 {
8688 /**
8689 * Tells whether the user can access and manipulate
8690 * the plugin's settings
8691 *
8692 * @return bool
8693 */
8694 public function mayModifySettings();
8695 /**
8696 * Tells whether the user can access the website with Phast enabled in preview mode
8697 *
8698 * @return bool
8699 */
8700 public function seesPreviewMode();
8701 }
8702 namespace Kibo\PhastPlugins\SDK\Security;
8703
8704 /**
8705 * Checks whether posted data to the server
8706 * contains the expected nonce.
8707 *
8708 * Interface NonceChecker
8709 */
8710 interface NonceChecker
8711 {
8712 /**
8713 * Performs the check
8714 *
8715 * @param array $data The data posted to the server
8716 * @return bool TRUE if all is well, FALSE otherwise
8717 */
8718 public function checkNonce(array $data);
8719 }
8720 namespace Kibo\PhastPlugins\SDK\Common;
8721
8722 /**
8723 * Contains common implementations for methods
8724 * of the PluginHost interface
8725 *
8726 * @see PluginHost
8727 * Trait PluginHostTrait
8728 */
8729 trait PluginHostTrait
8730 {
8731 public function getPluginName()
8732 {
8733 return 'Phast';
8734 }
8735 public function isDev()
8736 {
8737 return $this->getPluginHostVersion() === '$VER' . 'SION$';
8738 }
8739 public function onPhastConfigurationLoad(array $config)
8740 {
8741 return $config;
8742 }
8743 public function getLocale()
8744 {
8745 return 'en';
8746 }
8747 public function getInstallNoticeRenderer()
8748 {
8749 return new \Kibo\PhastPlugins\SDK\AdminPanel\DefaultInstallNoticeRenderer();
8750 }
8751 }
8752 namespace Kibo\PhastPlugins\SDK\Common;
8753
8754 trait ServiceHostTrait
8755 {
8756 public function onServiceConfigurationLoad(array $config)
8757 {
8758 return $config;
8759 }
8760 }
8761 namespace Kibo\PhastPlugins\SDK\Common;
8762
8763 trait PreviewCookieTrait
8764 {
8765 public function seesPreviewMode()
8766 {
8767 return isset($_COOKIE['PHAST_PREVIEW']) && (bool) $_COOKIE['PHAST_PREVIEW'];
8768 }
8769 }
8770 namespace Kibo\Phast\Environment\Exceptions;
8771
8772 class PackageHasNoDiagnosticsException extends \Kibo\Phast\Exceptions\LogicException
8773 {
8774 }
8775 namespace Kibo\Phast\Environment\Exceptions;
8776
8777 class PackageHasNoFactoryException extends \Kibo\Phast\Exceptions\LogicException
8778 {
8779 }
8780 namespace Kibo\Phast\Cache\File;
8781
8782 class DiagnosticsLogWriter implements \Kibo\Phast\Logging\LogWriter
8783 {
8784 public function setLevelMask($mask)
8785 {
8786 }
8787 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry)
8788 {
8789 if ($entry->getLevel() > 2) {
8790 $needles = array_map(function ($key) {
8791 return '{' . $key . '}';
8792 }, array_keys($entry->getContext()));
8793 $message = str_replace($needles, $entry->getContext(), $entry->getMessage());
8794 throw new \Kibo\Phast\Exceptions\RuntimeException("Error: Level: {$entry->getLevel()}, Msg: {$message}");
8795 }
8796 }
8797 }
8798 namespace Kibo\Phast\Cache\File;
8799
8800 class Diagnostics implements \Kibo\Phast\Diagnostics\Diagnostics
8801 {
8802 public function diagnose(array $config)
8803 {
8804 \Kibo\Phast\Logging\Log::setLogger(new \Kibo\Phast\Logging\Logger(new \Kibo\Phast\Cache\File\DiagnosticsLogWriter()));
8805 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'cache-self-diagnosis');
8806 $v1 = $cache->get('test-key', function () {
8807 return 1;
8808 }, 2);
8809 $v2 = $cache->get('test-key', function () {
8810 return 2;
8811 }, 2);
8812 if ($v1 != $v2) {
8813 throw new \Kibo\Phast\Exceptions\RuntimeException('Cache failed, but no error was reported!');
8814 }
8815 }
8816 }
8817 namespace Kibo\Phast\Filters\HTML\CommentsRemoval;
8818
8819 class Filter implements \Kibo\Phast\Filters\HTML\HTMLStreamFilter
8820 {
8821 public function transformElements(\Traversable $elements, \Kibo\Phast\Filters\HTML\HTMLPageContext $context)
8822 {
8823 foreach ($elements as $element) {
8824 if (!$element instanceof \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Comment || $element->isIEConditional()) {
8825 (yield $element);
8826 }
8827 }
8828 }
8829 }
8830 namespace Kibo\Phast\Filters\HTML\ScriptsDeferring;
8831
8832 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
8833 {
8834 use \Kibo\Phast\Filters\HTML\Helpers\JSDetectorTrait;
8835 protected function isTagOfInterest(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
8836 {
8837 return $tag->getTagName() == 'script';
8838 }
8839 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
8840 {
8841 if ($this->isJSElement($script) && !$this->isDeferralDisabled($script)) {
8842 $this->rewrite($script);
8843 }
8844 (yield $script);
8845 }
8846 protected function afterLoop()
8847 {
8848 $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;var hasCurrentScript=!!document.currentScript;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\")};if(hasCurrentScript){try{Object.defineProperty(s,\"currentScript\",{configurable:true,get:function(){return \$}})}catch(ia){console.error(\"[Phast] Unable to override document.currentScript on this browser: \",ia)}}function da(ja){var ka=s.createElement(\"div\");ka.innerHTML=ja;var la=ea(ka);if(aa&&aa.parentNode!==\$.parentNode){aa=\$.nextElementSibling}while(ka.firstChild){\$.parentNode.insertBefore(ka.firstChild,aa)}la.map(fa)}function ea(ma){return Array.prototype.slice.call(ma.getElementsByTagName(\"script\")).filter(function(na){var oa=na.getAttribute(\"type\");return!oa||/^(text|application)\\/javascript(;|\$)/i.test(oa)})}function fa(pa){var qa=new phast.ScriptsLoader.Scripts.Factory(s);var ra=qa.makeScriptFromElement(pa);ba=phast.ScriptsLoader.chainScript(ba,ra)}return _().then(function(){return ba}).finally(function(){delete s.write;delete s.writeln;if(hasCurrentScript){delete s.currentScript}})}function B(sa){var ta=s.createElement(\"link\");ta.setAttribute(\"rel\",\"preload\");ta.setAttribute(\"as\",\"script\");ta.setAttribute(\"href\",sa);s.head.appendChild(ta)}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(ua,va){this._utils=ua;this._element=va;this.init=function(){return Promise.resolve()};this.execute=function(){var wa=va.textContent.replace(/^\\s*<!--.*\\n/i,\"\");ua.restoreOriginals(va);return ua.writeProtectAndExecuteString(va,wa)};this.describe=function(){return\"inline script\"}};phast.ScriptsLoader.Scripts.AsyncBrowserScript=function(xa,ya){var za;this._utils=xa;this._element=ya;this.init=function(){xa.addPreload(getSrc(ya));return new Promise(function(Aa){za=Aa})};this.execute=function(){var Ba=xa.copyElement(ya);xa.restoreOriginals(Ba);xa.replaceElement(ya,Ba).then(za).catch(za);return Promise.resolve()};this.describe=function(){return\"async script at \"+getSrc(ya)}};phast.ScriptsLoader.Scripts.SyncBrowserScript=function(Ca,Da){this._utils=Ca;this._element=Da;this.init=function(){Ca.addPreload(getSrc(Da));return Promise.resolve()};this.execute=function(){var Ea=Ca.copyElement(Da);Ca.restoreOriginals(Ea);return Ca.writeProtectAndReplaceElement(Da,Ea)};this.describe=function(){return\"sync script at \"+getSrc(Da)}};phast.ScriptsLoader.Scripts.AsyncAJAXScript=function(Fa,Ga,Ha,Ia){this._utils=Fa;this._element=Ga;this._fetch=Ha;this._fallback=Ia;var Ja;var Ka;this.init=function(){Ja=Ha(Ga);return new Promise(function(La){Ka=La})};this.execute=function(){Ja.then(function(Ma){Fa.restoreOriginals(Ga);return Fa.writeProtectAndExecuteString(Ga,Ma).then(Ka)}).catch(function(){Ia.init();return Ia.execute().then(Ka)});return Promise.resolve()};this.describe=function(){return\"bundled async script at \"+Ga.getAttribute(\"data-phast-original-src\")}};phast.ScriptsLoader.Scripts.SyncAJAXScript=function(Na,Oa,Pa,Qa){this._utils=Na;this._element=Oa;this._fetch=Pa;this._fallback=Qa;var Ra;this.init=function(){Ra=Pa(Oa);return Ra};this.execute=function(){return Ra.then(function(Sa){Na.restoreOriginals(Oa);return Na.writeProtectAndExecuteString(Oa,Sa)}).catch(function(){Qa.init();return Qa.execute()})};this.describe=function(){return\"bundled sync script at \"+Oa.getAttribute(\"data-phast-original-src\")}};phast.ScriptsLoader.Scripts.Factory=function(Ta,Ua){var Va=phast.ScriptsLoader.Scripts;var Wa=new phast.ScriptsLoader.Utilities(Ta);this.makeScriptFromElement=function(Za){var \$a;if(Xa(Za)){if(isAsync(Za)){\$a=new Va.AsyncBrowserScript(Wa,Za);return Ua?new Va.AsyncAJAXScript(Wa,Za,Ua,\$a):\$a}\$a=new Va.SyncBrowserScript(Wa,Za);return Ua?new Va.SyncAJAXScript(Wa,Za,Ua,\$a):\$a}if(Ya(Za)){return new Va.InlineScript(Wa,Za)}if(isAsync(Za)){return new Va.AsyncBrowserScript(Wa,Za)}return new Va.SyncBrowserScript(Wa,Za)};function Xa(_a){return _a.hasAttribute(\"data-phast-params\")}function Ya(ab){return!ab.hasAttribute(\"src\")}};function getSrc(bb){if(bb.hasAttribute(\"data-phast-original-src\")){return bb.getAttribute(\"data-phast-original-src\")}else if(bb.hasAttribute(\"src\")){return bb.getAttribute(\"src\")}}function isAsync(cb){return cb.hasAttribute(\"async\")}function isDefer(db){return db.hasAttribute(\"defer\")}\n"));
8849 $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});document.addEventListener(\"readystatechange\",function(b){if(document.readyState===\"loading\"){b.stopImmediatePropagation()}});var didSetTimeout=false;var originalSetTimeout=window.setTimeout;window.setTimeout=function(c,d){if(!d||d<0){didSetTimeout=true}return originalSetTimeout.call(window,c,d)};function loadScripts(){var e=new phast.ScriptsLoader.Scripts.Factory(document,fetchScript);var f=phast.ScriptsLoader.getScriptsInExecutionOrder(document,e);if(f.length===0){return}setReadyState(\"loading\");phast.ScriptsLoader.executeScripts(f).then(restoreReadyState)}function setReadyState(g){try{Object.defineProperty(document,\"readyState\",{configurable:true,get:function(){return g}})}catch(h){console.error(\"[Phast] Unable to override document.readyState on this browser: \",h)}}function restoreReadyState(){i().then(function(){setReadyState(\"interactive\");triggerEvent(document,\"readystatechange\");return i()}).then(function(){triggerEvent(document,\"DOMContentLoaded\");return i()}).then(function(){delete document[\"readyState\"];triggerEvent(document,\"readystatechange\");if(loadFiltered){triggerEvent(window,\"load\")}loadFiltered=true});function i(){return new Promise(function(j){(function k(l){if(didSetTimeout&&l<10){didSetTimeout=false;originalSetTimeout.call(window,function(){k(l+1)})}else{requestAnimationFrame(j)}})(0)})}}function triggerEvent(m,n){var o=document.createEvent(\"Event\");o.initEvent(n,true,true);m.dispatchEvent(o)}function fetchScript(p){return phast.ResourceLoader.instance.get(phast.ResourceLoader.RequestParams.fromString(p.getAttribute(\"data-phast-params\")))}\n"));
8850 }
8851 private function rewrite(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
8852 {
8853 if ($script->hasAttribute('type')) {
8854 $script->setAttribute('data-phast-original-type', $script->getAttribute('type'));
8855 }
8856 $script->setAttribute('type', 'text/phast');
8857 if ($script->hasAttribute('data-phast-params')) {
8858 $script->removeAttribute('src');
8859 }
8860 }
8861 private function isDeferralDisabled(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $script)
8862 {
8863 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);
8864 }
8865 }
8866 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\Tags;
8867
8868 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8869 {
8870 public function make(array $config)
8871 {
8872 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));
8873 }
8874 }
8875 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS;
8876
8877 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8878 {
8879 public function make(array $config)
8880 {
8881 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));
8882 }
8883 }
8884 namespace Kibo\Phast\Filters\HTML\ImagesOptimizationService\CSS;
8885
8886 class Filter extends \Kibo\Phast\Filters\HTML\BaseHTMLStreamFilter
8887 {
8888 /**
8889 * @var ImageURLRewriter
8890 */
8891 protected $rewriter;
8892 /**
8893 * Filter constructor.
8894 * @param ImageURLRewriter $rewriter
8895 */
8896 public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter)
8897 {
8898 $this->rewriter = $rewriter;
8899 }
8900 protected function handleTag(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Tag $tag)
8901 {
8902 if ($tag->hasAttribute('style')) {
8903 $tag->setAttribute('style', $this->rewriter->rewriteStyle($tag->getAttribute('style')));
8904 }
8905 (yield $tag);
8906 }
8907 }
8908 namespace Kibo\Phast\Filters\HTML\PhastScriptsCompiler;
8909
8910 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8911 {
8912 public function make(array $config)
8913 {
8914 $cache = new \Kibo\Phast\Cache\File\Cache($config['cache'], 'phast-scripts');
8915 $compiler = new \Kibo\Phast\Filters\HTML\PhastScriptsCompiler\PhastJavaScriptCompiler($cache, $config['servicesUrl'], $config['serviceRequestFormat']);
8916 return new \Kibo\Phast\Filters\HTML\PhastScriptsCompiler\Filter($compiler);
8917 }
8918 }
8919 namespace Kibo\Phast\Filters\HTML\ScriptsProxyService;
8920
8921 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8922 {
8923 public function make(array $config)
8924 {
8925 if (!isset($config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class]['serviceUrl'])) {
8926 $config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class]['serviceUrl'] = $config['servicesUrl'];
8927 }
8928 $filterConfig = $config['documents']['filters'][\Kibo\Phast\Filters\HTML\ScriptsProxyService\Filter::class];
8929 $filterConfig['match'] = $config['scripts']['whitelist'];
8930 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));
8931 }
8932 }
8933 namespace Kibo\Phast\Filters\HTML\CSSInlining;
8934
8935 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8936 {
8937 public function make(array $config)
8938 {
8939 $localRetriever = new \Kibo\Phast\Retrievers\LocalRetriever($config['retrieverMap']);
8940 $retriever = new \Kibo\Phast\Retrievers\UniversalRetriever();
8941 $retriever->addRetriever($localRetriever);
8942 $retriever->addRetriever(new \Kibo\Phast\Retrievers\CachingRetriever(new \Kibo\Phast\Cache\File\Cache($config['cache'], 'css')));
8943 if (!isset($config['documents']['filters'][\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class]['serviceUrl'])) {
8944 $config['documents']['filters'][\Kibo\Phast\Filters\HTML\CSSInlining\Filter::class]['serviceUrl'] = $config['servicesUrl'];
8945 }
8946 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));
8947 }
8948 }
8949 namespace Kibo\Phast\Filters\HTML\Diagnostics;
8950
8951 class Factory implements \Kibo\Phast\Filters\HTML\HTMLFilterFactory
8952 {
8953 public function make(array $config)
8954 {
8955 $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';
8956 return new \Kibo\Phast\Filters\HTML\Diagnostics\Filter($url);
8957 }
8958 }
8959 namespace Kibo\Phast\Filters\Image\Exceptions;
8960
8961 class ImageProcessingException extends \Kibo\Phast\Exceptions\RuntimeException
8962 {
8963 }
8964 namespace Kibo\Phast\Filters\Image\ImageImplementations;
8965
8966 class DummyImage extends \Kibo\Phast\Filters\Image\ImageImplementations\BaseImage implements \Kibo\Phast\Filters\Image\Image
8967 {
8968 /**
8969 * @var string
8970 */
8971 private $imageString;
8972 private $transformationString;
8973 /**
8974 * DummyImage constructor.
8975 *
8976 * @param int $width
8977 * @param int $height
8978 */
8979 public function __construct($width = null, $height = null)
8980 {
8981 $this->width = $width;
8982 $this->height = $height;
8983 }
8984 /**
8985 * @return int
8986 */
8987 public function getWidth()
8988 {
8989 return $this->width;
8990 }
8991 /**
8992 * @return int
8993 */
8994 public function getHeight()
8995 {
8996 return $this->height;
8997 }
8998 /**
8999 * @return string
9000 */
9001 public function getType()
9002 {
9003 return $this->type;
9004 }
9005 /**
9006 * @param string $type
9007 */
9008 public function setType($type)
9009 {
9010 $this->type = $type;
9011 }
9012 /**
9013 * @return int
9014 */
9015 public function getCompression()
9016 {
9017 return $this->compression;
9018 }
9019 /**
9020 * @return string
9021 */
9022 public function getAsString()
9023 {
9024 return $this->imageString;
9025 }
9026 /**
9027 * @param string $imageString
9028 */
9029 public function setImageString($imageString)
9030 {
9031 $this->imageString = $imageString;
9032 }
9033 /**
9034 * @param mixed $transformationString
9035 */
9036 public function setTransformationString($transformationString)
9037 {
9038 $this->transformationString = $transformationString;
9039 }
9040 protected function __clone()
9041 {
9042 $this->imageString = $this->transformationString;
9043 }
9044 }
9045 namespace Kibo\Phast\Filters\Service;
9046
9047 class CachingServiceFilter implements \Kibo\Phast\Services\ServiceFilter
9048 {
9049 use \Kibo\Phast\Logging\LoggingTrait;
9050 /**
9051 * @var Cache
9052 */
9053 private $cache;
9054 /**
9055 * @var CachedResultServiceFilter
9056 */
9057 private $cachedFilter;
9058 /**
9059 * @var Retriever
9060 */
9061 private $retriever;
9062 /**
9063 * CachingServiceFilter constructor.
9064 * @param Cache $cache
9065 * @param CachedResultServiceFilter $cachedFilter
9066 * @param Retriever $retriever
9067 */
9068 public function __construct(\Kibo\Phast\Cache\Cache $cache, \Kibo\Phast\Filters\Service\CachedResultServiceFilter $cachedFilter, \Kibo\Phast\Retrievers\Retriever $retriever)
9069 {
9070 $this->cache = $cache;
9071 $this->cachedFilter = $cachedFilter;
9072 $this->retriever = $retriever;
9073 }
9074 /**
9075 * @param Resource $resource
9076 * @param array $request
9077 * @return Resource
9078 * @throws CachedExceptionException
9079 */
9080 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9081 {
9082 $key = $this->cachedFilter->getCacheSalt($resource, $request);
9083 $this->logger()->info('Trying to get {url} from cache', ['url' => (string) $resource->getUrl()]);
9084 $result = $this->cache->get($key);
9085 if (isset($result['encoding']) && $result['encoding'] != 'identity') {
9086 $result = null;
9087 }
9088 if ($result && $this->checkDependencies($result)) {
9089 return $this->deserializeCachedData($result);
9090 }
9091 try {
9092 $result = $this->cachedFilter->apply($resource, $request);
9093 $this->cache->set($key, $this->serializeResource($result));
9094 return $result;
9095 } catch (\Exception $e) {
9096 $cachingException = $this->serializeException($e);
9097 $this->cache->set($key, $cachingException);
9098 throw $this->deserializeException($cachingException);
9099 }
9100 }
9101 private function checkDependencies(array $data)
9102 {
9103 foreach ((array) @$data['dependencies'] as $dep) {
9104 $url = \Kibo\Phast\ValueObjects\URL::fromString($dep['url']);
9105 if ($this->retriever->getCacheSalt($url) >= $dep['cacheMarker']) {
9106 return false;
9107 }
9108 }
9109 return true;
9110 }
9111 private function deserializeCachedData(array $data)
9112 {
9113 if ($data['dataType'] == 'exception') {
9114 throw $this->deserializeException($data);
9115 }
9116 return $this->deserializeResource($data);
9117 }
9118 private function serializeResource(\Kibo\Phast\ValueObjects\Resource $resource)
9119 {
9120 return ['dataType' => 'resource', 'url' => $resource->getUrl()->toString(), 'mimeType' => $resource->getMimeType(), 'blob' => base64_encode($resource->getContent()), 'dependencies' => $this->serializeDependencies($resource)];
9121 }
9122 private function serializeDependencies(\Kibo\Phast\ValueObjects\Resource $resource)
9123 {
9124 return array_map(function (\Kibo\Phast\ValueObjects\Resource $dep) {
9125 return ['url' => $dep->getUrl()->toString(), 'cacheMarker' => $dep->getCacheSalt()];
9126 }, $resource->getDependencies());
9127 }
9128 private function deserializeResource(array $data)
9129 {
9130 $params = [\Kibo\Phast\ValueObjects\URL::fromString($data['url']), base64_decode($data['blob']), $data['mimeType']];
9131 return \Kibo\Phast\ValueObjects\Resource::makeWithContent(...$params);
9132 }
9133 private function serializeException(\Exception $e)
9134 {
9135 return ['dataType' => 'exception', 'class' => get_class($e), 'msg' => $e->getMessage(), 'code' => $e->getCode()];
9136 }
9137 private function deserializeException(array $data)
9138 {
9139 return new \Kibo\Phast\Exceptions\CachedExceptionException(sprintf('Phast: %s: Type: %s, Msg: %s, Code: %s', static::class, $data['class'], $data['msg'], $data['code']));
9140 }
9141 }
9142 namespace Kibo\Phast\Filters\Service;
9143
9144 interface CachedResultServiceFilter extends \Kibo\Phast\Services\ServiceFilter
9145 {
9146 /**
9147 * @param Resource $resource
9148 * @param array $request
9149 * @return string
9150 */
9151 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request);
9152 }
9153 namespace Kibo\Phast\Filters\Service;
9154
9155 class CompositeFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9156 {
9157 use \Kibo\Phast\Logging\LoggingTrait;
9158 /**
9159 * @var ServiceFilter[]
9160 */
9161 private $filters = array();
9162 public function addFilter(\Kibo\Phast\Services\ServiceFilter $filter)
9163 {
9164 $this->filters[] = $filter;
9165 }
9166 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9167 {
9168 $classes = array_map('get_class', $this->filters);
9169 $cached = array_filter($this->filters, function (\Kibo\Phast\Services\ServiceFilter $filter) {
9170 return $filter instanceof \Kibo\Phast\Filters\Service\CachedResultServiceFilter;
9171 });
9172 $salts = array_map(function (\Kibo\Phast\Filters\Service\CachedResultServiceFilter $filter) use($resource, $request) {
9173 return $filter->getCacheSalt($resource, $request);
9174 }, $cached);
9175 return join("\n", array_merge($classes, $salts, [$resource->getUrl(), $resource->getCacheSalt()]));
9176 }
9177 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9178 {
9179 $this->logger()->info('Starting filtering for resource {url}', ['url' => $resource->getUrl()]);
9180 $result = array_reduce($this->filters, function (\Kibo\Phast\ValueObjects\Resource $resource, \Kibo\Phast\Services\ServiceFilter $filter) use($request) {
9181 $this->logger()->info('Starting {filter}', ['filter' => get_class($filter)]);
9182 try {
9183 return $filter->apply($resource, $request);
9184 } catch (\Kibo\Phast\Exceptions\RuntimeException $e) {
9185 $message = 'Phast RuntimeException: Filter: {filter} Exception: {exceptionClass} Msg: {message} Code: {code} File: {file} Line: {line}';
9186 $this->logger()->critical($message, ['filter' => get_class($filter), 'exceptionClass' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
9187 return $resource;
9188 }
9189 }, $resource);
9190 $this->logger()->info('Done filtering for resource {url}', ['url' => $resource->getUrl()]);
9191 return $result;
9192 }
9193 }
9194 namespace Kibo\Phast\Filters\CSS\CSSMinifier;
9195
9196 class Filter implements \Kibo\Phast\Services\ServiceFilter
9197 {
9198 /**
9199 * @param Resource $resource
9200 * @param array $request
9201 * @return Resource
9202 */
9203 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9204 {
9205 $content = $resource->getContent();
9206 // Normalize whitespace
9207 $content = preg_replace('~\\s+~', ' ', $content);
9208 // Remove whitespace before and after operators
9209 $chars = [',', '{', '}', ';'];
9210 foreach ($chars as $char) {
9211 $content = str_replace("{$char} ", $char, $content);
9212 $content = str_replace(" {$char}", $char, $content);
9213 }
9214 // Remove whitespace after colons
9215 $content = str_replace(': ', ':', $content);
9216 return $resource->withContent(trim($content));
9217 }
9218 }
9219 namespace Kibo\Phast\Filters\CSS\CSSURLRewriter;
9220
9221 class Filter implements \Kibo\Phast\Services\ServiceFilter
9222 {
9223 /**
9224 * @param Resource $resource
9225 * @param array $request
9226 * @return Resource
9227 */
9228 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9229 {
9230 $baseUrl = $resource->getUrl();
9231 $callback = function ($match) use($baseUrl) {
9232 if (preg_match('~^[a-z]+:|^#~i', $match[3])) {
9233 return $match[0];
9234 }
9235 return $match[1] . \Kibo\Phast\ValueObjects\URL::fromString($match[3])->withBase($baseUrl) . $match[4];
9236 };
9237 $cssContent = preg_replace_callback('~
9238 \\b
9239 ( url\\( ([\'"]?) )
9240 ([A-Za-z0-9_/.:?&=+%,#@-]+)
9241 ( \\2 \\) )
9242 ~x', $callback, $resource->getContent());
9243 $cssContent = preg_replace_callback('~
9244 ( @import \\s+ ([\'"]) )
9245 ([A-Za-z0-9_/.:?&=+%,#@-]+)
9246 ( \\2 )
9247 ~x', $callback, $cssContent);
9248 return $resource->withContent($cssContent);
9249 }
9250 }
9251 namespace Kibo\Phast\Filters\CSS\ImageURLRewriter;
9252
9253 class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9254 {
9255 /**
9256 * @var ImageURLRewriter
9257 */
9258 private $rewriter;
9259 /**
9260 * Filter constructor.
9261 * @param ImageURLRewriter $rewriter
9262 */
9263 public function __construct(\Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageURLRewriter $rewriter)
9264 {
9265 $this->rewriter = $rewriter;
9266 }
9267 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9268 {
9269 return $this->rewriter->getCacheSalt();
9270 }
9271 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9272 {
9273 $content = $this->rewriter->rewriteStyle($resource->getContent());
9274 $dependencies = $this->rewriter->getInlinedResources();
9275 return $resource->withContent($content)->withDependencies($dependencies);
9276 }
9277 }
9278 namespace Kibo\Phast\Filters\CSS\Composite;
9279
9280 class Filter extends \Kibo\Phast\Filters\Service\CompositeFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9281 {
9282 public function __construct()
9283 {
9284 $this->addFilter(new \Kibo\Phast\Filters\CSS\CommentsRemoval\Filter());
9285 }
9286 }
9287 namespace Kibo\Phast\Filters\CSS\FontSwap;
9288
9289 class Filter implements \Kibo\Phast\Services\ServiceFilter
9290 {
9291 const FONT_FACE_REGEXP = '/(@font-face\\s*\\{)([^}]*)/i';
9292 const ICON_FONT_FAMILIES = array('Font Awesome', 'GeneratePress', 'Dashicons', 'Ionicons');
9293 private $fontDisplayBlockPattern;
9294 public function __construct()
9295 {
9296 $this->fontDisplayBlockPattern = $this->getFontDisplayBlockPattern();
9297 }
9298 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9299 {
9300 $css = $resource->getContent();
9301 $filtered = preg_replace_callback(self::FONT_FACE_REGEXP, function ($match) {
9302 list($block, $start, $contents) = $match;
9303 $mode = preg_match($this->fontDisplayBlockPattern, $contents) ? 'block' : 'swap';
9304 return $start . 'font-display:' . $mode . ';' . $contents;
9305 }, $css);
9306 return $resource->withContent($filtered);
9307 }
9308 private function getFontDisplayBlockPattern()
9309 {
9310 $patterns = [];
9311 foreach (self::ICON_FONT_FAMILIES as $family) {
9312 $chars = str_split($family);
9313 $chars = array_map(function ($char) {
9314 return preg_quote($char, '~');
9315 }, $chars);
9316 $patterns[] = implode('\\s*', $chars);
9317 }
9318 return '~' . implode('|', $patterns) . '~i';
9319 }
9320 }
9321 namespace Kibo\Phast\Filters\CSS\ImportsStripper;
9322
9323 class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9324 {
9325 use \Kibo\Phast\Logging\LoggingTrait;
9326 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9327 {
9328 return $this->shouldStripImports($request) ? 'strip-imports' : 'no-strip-imports';
9329 }
9330 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9331 {
9332 if (!$this->shouldStripImports($request)) {
9333 $this->logger()->info('No import stripping requested! Skipping!');
9334 return $resource;
9335 }
9336 $css = $resource->getContent();
9337 $stripped = preg_replace(\Kibo\Phast\Filters\HTML\CSSInlining\Filter::CSS_IMPORTS_REGEXP, '', $css);
9338 return $resource->withContent($stripped);
9339 }
9340 private function shouldStripImports(array $request)
9341 {
9342 return isset($request['strip-imports']);
9343 }
9344 }
9345 namespace Kibo\Phast\Filters\CSS\CommentsRemoval;
9346
9347 class Filter implements \Kibo\Phast\Services\ServiceFilter
9348 {
9349 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9350 {
9351 $content = preg_replace('~/\\*[^*]*\\*+([^/*][^*]*\\*+)*/~', '', $resource->getContent());
9352 return $resource->withContent($content);
9353 }
9354 }
9355 namespace Kibo\Phast\Filters\Text\Decode;
9356
9357 class Filter implements \Kibo\Phast\Services\ServiceFilter
9358 {
9359 const UTF8_BOM = "\357\273\277";
9360 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request = array())
9361 {
9362 $content = $resource->getContent();
9363 if (substr($content, 0, strlen(self::UTF8_BOM)) == self::UTF8_BOM) {
9364 $content = substr($content, strlen(self::UTF8_BOM));
9365 }
9366 return $resource->withContent($content);
9367 }
9368 }
9369 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
9370
9371 class ClosingTag extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element
9372 {
9373 /**
9374 * @var string
9375 */
9376 private $tagName;
9377 /**
9378 * ClosingTag constructor.
9379 * @param string $tagName
9380 */
9381 public function __construct($tagName)
9382 {
9383 $this->tagName = strtolower($tagName);
9384 }
9385 /**
9386 * @return string
9387 */
9388 public function getTagName()
9389 {
9390 return $this->tagName;
9391 }
9392 public function appendChild(\Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element $element)
9393 {
9394 $this->stream->insertBeforeElement($this, $element);
9395 }
9396 public function dumpValue()
9397 {
9398 return $this->tagName;
9399 }
9400 }
9401 namespace Kibo\Phast\Parsing\HTML\HTMLStreamElements;
9402
9403 class Tag extends \Kibo\Phast\Parsing\HTML\HTMLStreamElements\Element
9404 {
9405 /**
9406 * @var string
9407 */
9408 private $tagName;
9409 /**
9410 * @var array
9411 */
9412 private $attributes = array();
9413 /**
9414 * @var array
9415 */
9416 private $newAttributes = array();
9417 /**
9418 * @var \Iterator
9419 */
9420 private $attributeReader;
9421 /**
9422 * @var string
9423 */
9424 private $textContent = '';
9425 /**
9426 * @var string
9427 */
9428 private $closingTag = '';
9429 private $dirty = false;
9430 /**
9431 * Tag constructor.
9432 * @param $tagName
9433 * @param array|\Traversable $attributes
9434 */
9435 public function __construct($tagName, $attributes = array())
9436 {
9437 $this->tagName = strtolower($tagName);
9438 if ($attributes instanceof \Iterator) {
9439 $this->attributeReader = $attributes;
9440 } elseif (is_array($attributes)) {
9441 $this->attributeReader = new \ArrayIterator($attributes);
9442 } else {
9443 throw new \InvalidArgumentException('Attributes must be array or Iterator');
9444 }
9445 }
9446 /**
9447 * @return string
9448 */
9449 public function getTagName()
9450 {
9451 return $this->tagName;
9452 }
9453 /**
9454 * @param string $attrName
9455 * @return bool
9456 */
9457 public function hasAttribute($attrName)
9458 {
9459 return $this->getAttribute($attrName) !== null;
9460 }
9461 /**
9462 * @param string $attrName
9463 * @return mixed|null
9464 */
9465 public function getAttribute($attrName)
9466 {
9467 if (array_key_exists($attrName, $this->newAttributes)) {
9468 return $this->newAttributes[$attrName];
9469 }
9470 if (!array_key_exists($attrName, $this->attributes)) {
9471 $this->readUntilAttribute($attrName);
9472 }
9473 if (isset($this->attributes[$attrName])) {
9474 return $this->attributes[$attrName];
9475 }
9476 }
9477 /** @return string[] */
9478 public function getAttributes()
9479 {
9480 $this->readUntilAttribute(null);
9481 return array_filter($this->newAttributes + $this->attributes, function ($value) {
9482 return $value !== null;
9483 });
9484 }
9485 private function readUntilAttribute($attrName)
9486 {
9487 if (!$this->attributeReader) {
9488 return;
9489 }
9490 while ($this->attributeReader->valid()) {
9491 $name = strtolower($this->attributeReader->key());
9492 $value = $this->attributeReader->current();
9493 $this->attributeReader->next();
9494 if (!isset($this->attributes[$name])) {
9495 $this->attributes[$name] = $value;
9496 }
9497 if ($name == $attrName) {
9498 return;
9499 }
9500 }
9501 $this->attributeReader = null;
9502 }
9503 /**
9504 * @param string $attrName
9505 * @param string $value
9506 */
9507 public function setAttribute($attrName, $value)
9508 {
9509 if ($this->getAttribute($attrName) === $value) {
9510 return;
9511 }
9512 $this->dirty = true;
9513 $this->newAttributes[$attrName] = $value;
9514 }
9515 /**
9516 * @param string $attrName
9517 */
9518 public function removeAttribute($attrName)
9519 {
9520 $this->dirty = true;
9521 $this->newAttributes[$attrName] = null;
9522 }
9523 /**
9524 * @return string
9525 */
9526 public function getTextContent()
9527 {
9528 return $this->textContent;
9529 }
9530 /**
9531 * @param string $textContent
9532 */
9533 public function setTextContent($textContent)
9534 {
9535 $this->textContent = $textContent;
9536 }
9537 /**
9538 * @param $closingTag
9539 * @return Tag
9540 */
9541 public function withClosingTag($closingTag)
9542 {
9543 $new = clone $this;
9544 $new->closingTag = $closingTag;
9545 return $new;
9546 }
9547 /**
9548 * @return string
9549 */
9550 public function getClosingTag()
9551 {
9552 return $this->closingTag;
9553 }
9554 public function __toString()
9555 {
9556 return $this->getOpening() . $this->textContent . $this->getClosing();
9557 }
9558 private function getOpening()
9559 {
9560 if ($this->dirty || !isset($this->originalString)) {
9561 return $this->generateOpeningTag();
9562 }
9563 return parent::__toString();
9564 }
9565 private function getClosing()
9566 {
9567 if ($this->closingTag) {
9568 return $this->closingTag;
9569 }
9570 if ($this->mustHaveClosing() && !$this->isFromParser()) {
9571 return '</' . $this->tagName . '>';
9572 }
9573 return '';
9574 }
9575 private function generateOpeningTag()
9576 {
9577 $parts = ['<' . $this->tagName];
9578 foreach ($this->getAttributes() as $name => $value) {
9579 $parts[] = $this->generateAttribute($name, $value);
9580 }
9581 return join(' ', $parts) . '>';
9582 }
9583 private function generateAttribute($name, $value)
9584 {
9585 $result = $name;
9586 if ($value != '') {
9587 $result .= '=' . $this->quoteAttributeValue($value);
9588 }
9589 return $result;
9590 }
9591 private function quoteAttributeValue($value)
9592 {
9593 if (strpos($value, '"') === false) {
9594 return '"' . htmlspecialchars($value) . '"';
9595 }
9596 return "'" . str_replace(['&', "'"], ['&amp;', '&#039;'], $value) . "'";
9597 }
9598 private function mustHaveClosing()
9599 {
9600 return !\Kibo\Phast\Parsing\HTML\HTMLInfo::isA($this->tagName, \Kibo\Phast\Parsing\HTML\HTMLInfo::VOID_TAG);
9601 }
9602 private function isFromParser()
9603 {
9604 return isset($this->originalString);
9605 }
9606 public function dumpValue()
9607 {
9608 $o = $this->tagName;
9609 foreach ($this->attributes as $name => $_) {
9610 $o .= " {$name}=\"" . $this->getAttribute($name) . '"';
9611 }
9612 if ($this->textContent) {
9613 $o .= " content=[{$this->textContent}]";
9614 }
9615 return $o;
9616 }
9617 }
9618 namespace Kibo\Phast\Logging\LogWriters\Dummy;
9619
9620 class Writer implements \Kibo\Phast\Logging\LogWriter
9621 {
9622 public function setLevelMask($mask)
9623 {
9624 }
9625 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry)
9626 {
9627 }
9628 }
9629 namespace Kibo\Phast\Logging\LogWriters;
9630
9631 abstract class BaseLogWriter implements \Kibo\Phast\Logging\LogWriter
9632 {
9633 protected $levelMask = ~0;
9634 protected abstract function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry);
9635 public function setLevelMask($mask)
9636 {
9637 $this->levelMask = $mask;
9638 }
9639 public function writeEntry(\Kibo\Phast\Logging\LogEntry $entry)
9640 {
9641 if ($this->levelMask & $entry->getLevel()) {
9642 $this->doWriteEntry($entry);
9643 }
9644 }
9645 }
9646 namespace Kibo\Phast\Services\Css;
9647
9648 class Service extends \Kibo\Phast\Services\BaseService
9649 {
9650 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9651 {
9652 $response = parent::makeResponse($resource, $request);
9653 $response->setHeader('Content-Type', 'text/css');
9654 return $response;
9655 }
9656 }
9657 namespace Kibo\Phast\Services\Scripts;
9658
9659 class Service extends \Kibo\Phast\Services\BaseService
9660 {
9661 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9662 {
9663 $response = parent::makeResponse($resource, $request);
9664 $response->setHeader('Content-Type', 'application/javascript');
9665 return $response;
9666 }
9667 }
9668 namespace Kibo\Phast\Services\Images;
9669
9670 class Service extends \Kibo\Phast\Services\BaseService
9671 {
9672 protected function getParams(\Kibo\Phast\Services\ServiceRequest $request)
9673 {
9674 $params = parent::getParams($request);
9675 if ($this->proxySupportsAccept($request->getHTTPRequest())) {
9676 $params['varyAccept'] = true;
9677 if ($this->browserSupportsWebp($request->getHTTPRequest())) {
9678 $params['preferredType'] = \Kibo\Phast\Filters\Image\Image::TYPE_WEBP;
9679 \Kibo\Phast\Logging\Log::info('WebP will be served if possible!');
9680 }
9681 }
9682 return $params;
9683 }
9684 protected function makeResponse(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9685 {
9686 $response = parent::makeResponse($resource, $request);
9687 $srcUrl = $resource->getUrl();
9688 $response->setHeader('Link', "<{$srcUrl}>; rel=\"canonical\"");
9689 $response->setHeader('Content-Type', $resource->getMimeType());
9690 if ($resource->getMimeType() != \Kibo\Phast\Filters\Image\Image::TYPE_PNG && @$request['varyAccept']) {
9691 $response->setHeader('Vary', 'Accept');
9692 }
9693 return $response;
9694 }
9695 protected function validateIntegrity(\Kibo\Phast\Services\ServiceRequest $request)
9696 {
9697 if (!$this->config['images']['api-mode']) {
9698 parent::validateIntegrity($request);
9699 }
9700 }
9701 protected function validateWhitelisted(\Kibo\Phast\Services\ServiceRequest $request)
9702 {
9703 if (!$this->config['images']['api-mode']) {
9704 parent::validateWhitelisted($request);
9705 }
9706 }
9707 private function browserSupportsWebp(\Kibo\Phast\HTTP\Request $request)
9708 {
9709 return strpos($request->getHeader('accept'), 'image/webp') !== false;
9710 }
9711 private function proxySupportsAccept(\Kibo\Phast\HTTP\Request $request)
9712 {
9713 return !$request->isCloudflare();
9714 }
9715 }
9716 namespace Kibo\PhastPlugins\SDK\AdminPanel;
9717
9718 class DefaultInstallNoticeRenderer implements \Kibo\PhastPlugins\SDK\AdminPanel\InstallNoticeRenderer
9719 {
9720 public function render($notice, $onCloseJSFunction)
9721 {
9722 return $notice;
9723 }
9724 }
9725 namespace Kibo\PhastPlugins\SDK;
9726
9727 interface PluginHost extends \Kibo\PhastPlugins\SDK\ServiceHost
9728 {
9729 /**
9730 * The name of the plugin used for displaying to the users
9731 *
9732 * @return string
9733 */
9734 public function getPluginName();
9735 /**
9736 * The name of the host system
9737 *
9738 * @return string
9739 */
9740 public function getPluginHostName();
9741 /**
9742 * The version of the plugin
9743 *
9744 * @return string
9745 */
9746 public function getPluginHostVersion();
9747 /**
9748 * Tells whether we are in production or development mode.
9749 * In development mode static files will be loaded from a dev server.
9750 * In production mode static files will be loaded from a prebuilt source.
9751 *
9752 * @return bool - TRUE for development, FALSE for production
9753 */
9754 public function isDev();
9755 /**
9756 * @return KeyValueStore
9757 */
9758 public function getKeyValueStore();
9759 /**
9760 * @return InstallNoticeRenderer
9761 */
9762 public function getInstallNoticeRenderer();
9763 /**
9764 * @return HostURLs
9765 */
9766 public function getHostURLs();
9767 /**
9768 * @return Nonce
9769 */
9770 public function getNonce();
9771 /**
9772 * @return NonceChecker
9773 */
9774 public function getNonceChecker();
9775 /**
9776 * @return PhastUser
9777 */
9778 public function getPhastUser();
9779 /**
9780 * Called right after phast's configuration
9781 * has been loaded. Use it to modify the config
9782 * and take any other needed action before
9783 * the filters are applied.
9784 *
9785 * @param array $config - The configuration that has been loaded
9786 * @return array - The configuration to use for phast
9787 */
9788 public function onPhastConfigurationLoad(array $config);
9789 /**
9790 * Returns the current system locale
9791 *
9792 * @return string
9793 */
9794 public function getLocale();
9795 }
9796 namespace Kibo\Phast\Filters\JavaScript\Minification;
9797
9798 class JSMinifierFilter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9799 {
9800 const VERSION = 2;
9801 private $removeLicenseHeaders = true;
9802 /**
9803 * JSMinifierFilter constructor.
9804 * @param bool $removeLicenseHeaders
9805 */
9806 public function __construct($removeLicenseHeaders)
9807 {
9808 $this->removeLicenseHeaders = (bool) $removeLicenseHeaders;
9809 }
9810 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9811 {
9812 return http_build_query(['v' => self::VERSION, 'removeLicenseHeaders' => $this->removeLicenseHeaders]);
9813 }
9814 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9815 {
9816 $minified = (new \Kibo\Phast\Common\JSMinifier($resource->getContent(), $this->removeLicenseHeaders))->min();
9817 return $resource->withContent($minified);
9818 }
9819 }
9820 namespace Kibo\Phast\Filters\Image\Composite;
9821
9822 class Filter implements \Kibo\Phast\Filters\Service\CachedResultServiceFilter
9823 {
9824 use \Kibo\Phast\Logging\LoggingTrait;
9825 /**
9826 * @var ImageFactory
9827 */
9828 private $imageFactory;
9829 /**
9830 * @var ImageInliningManager
9831 */
9832 private $inliningManager;
9833 /**
9834 * @var ImageFilter[]
9835 */
9836 private $filters = array();
9837 /**
9838 * Filter constructor.
9839 * @param ImageFactory $imageFactory
9840 * @param ImageInliningManager $inliningManager
9841 */
9842 public function __construct(\Kibo\Phast\Filters\Image\ImageFactory $imageFactory, \Kibo\Phast\Filters\HTML\ImagesOptimizationService\ImageInliningManager $inliningManager)
9843 {
9844 $this->imageFactory = $imageFactory;
9845 $this->inliningManager = $inliningManager;
9846 }
9847 public function addImageFilter(\Kibo\Phast\Filters\Image\ImageFilter $filter)
9848 {
9849 $this->filters[] = $filter;
9850 }
9851 public function getCacheSalt(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9852 {
9853 $filters = array_map('get_class', $this->filters);
9854 $salts = array_map(function (\Kibo\Phast\Filters\Image\ImageFilter $filter) use($request) {
9855 return $filter->getCacheSalt($request);
9856 }, $this->filters);
9857 return implode("\n", array_merge($filters, $salts, [$this->inliningManager->getMaxImageInliningSize(), $resource->getUrl(), $resource->getCacheSalt()]));
9858 }
9859 /**
9860 * @param Resource $resource
9861 * @param array $request
9862 * @return Resource
9863 */
9864 public function apply(\Kibo\Phast\ValueObjects\Resource $resource, array $request)
9865 {
9866 $image = $this->imageFactory->getForResource($resource);
9867 $filteredImage = $image;
9868 foreach ($this->filters as $filter) {
9869 $this->logger()->info('Applying {filter}', ['filter' => get_class($filter)]);
9870 try {
9871 $filteredImage = $filter->transformImage($filteredImage, $request);
9872 } catch (\Kibo\Phast\Filters\Image\Exceptions\ImageProcessingException $e) {
9873 $message = 'Image filter exception: Filter: {filter} Exception: {exceptionClass} Msg: {message} Code: {code} File: {file} Line: {line}';
9874 $this->logger()->critical($message, ['filter' => get_class($filter), 'exceptionClass' => get_class($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile(), 'line' => $e->getLine()]);
9875 }
9876 }
9877 $sizeBefore = $filteredImage->getSizeAsString();
9878 $sizeAfter = $image->getSizeAsString();
9879 $sizeDifference = $sizeBefore - $sizeAfter;
9880 $this->logger()->info('Image processed. Size before/after: {sizeBefore}/{sizeAfter} ({sizeDifference})', ['sizeBefore' => $sizeBefore, 'sizeAfter' => $sizeAfter, 'sizeDifference' => $sizeDifference < 0 ? $sizeDifference : "+{$sizeDifference}"]);
9881 if ($sizeDifference < 0) {
9882 $this->logger()->info('Return filtered image and save {sizeDifference} bytes', ['sizeDifference' => -$sizeDifference]);
9883 $image = $filteredImage;
9884 } else {
9885 $this->logger()->info('Return original image');
9886 }
9887 $processedResource = $resource->withContent($image->getAsString(), $image->getType());
9888 $this->inliningManager->maybeStoreForInlining($processedResource);
9889 return $processedResource;
9890 }
9891 }
9892 namespace Kibo\Phast\Logging\LogWriters\JSONLFile;
9893
9894 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
9895 {
9896 use \Kibo\Phast\Logging\Common\JSONLFileLogTrait;
9897 /**
9898 * @param LogEntry $entry
9899 */
9900 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
9901 {
9902 $encoded = @\Kibo\Phast\Common\JSON::encode($entry->toArray());
9903 if ($encoded) {
9904 $this->makeDirIfNotExists();
9905 @file_put_contents($this->filename, $encoded . "\n", FILE_APPEND | LOCK_EX);
9906 }
9907 }
9908 private function makeDirIfNotExists()
9909 {
9910 if (!@file_exists($this->dir)) {
9911 @mkdir($this->dir, 0777, true);
9912 }
9913 }
9914 }
9915 namespace Kibo\Phast\Logging\LogWriters\Composite;
9916
9917 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
9918 {
9919 /**
9920 * @var Writer[]
9921 */
9922 private $writers = array();
9923 public function addWriter(\Kibo\Phast\Logging\LogWriter $writer)
9924 {
9925 $this->writers[] = $writer;
9926 }
9927 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
9928 {
9929 foreach ($this->writers as $writer) {
9930 $writer->writeEntry($entry);
9931 }
9932 }
9933 }
9934 namespace Kibo\Phast\Logging\LogWriters\RotatingTextFile;
9935
9936 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
9937 {
9938 /** @var string */
9939 private $path = 'phast.log';
9940 /** @var int */
9941 private $maxFiles = 2;
9942 /** @var int */
9943 private $maxSize = 10 * 1024 * 1024;
9944 /** @var ObjectifiedFunctions */
9945 private $funcs;
9946 /**
9947 * @param array $config
9948 * @param ?ObjectifiedFunctions $funcs
9949 */
9950 public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
9951 {
9952 if (isset($config['path'])) {
9953 $this->path = (string) $config['path'];
9954 }
9955 if (isset($config['maxFiles'])) {
9956 $this->maxFiles = (int) $config['maxFiles'];
9957 }
9958 if (isset($config['maxSize'])) {
9959 $this->maxSize = (int) $config['maxSize'];
9960 }
9961 $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs;
9962 }
9963 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
9964 {
9965 if (!($this->levelMask & $entry->getLevel())) {
9966 return;
9967 }
9968 $message = $this->interpolate($entry->getMessage(), $entry->getContext());
9969 $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);
9970 clearstatcache(true, $this->path);
9971 $this->rotate(strlen($line));
9972 file_put_contents($this->path, $line, FILE_APPEND);
9973 }
9974 private function interpolate($message, $context)
9975 {
9976 $prefix = '';
9977 $prefixKeys = ['requestId', 'service', 'class', 'method', 'line'];
9978 foreach ($prefixKeys as $key) {
9979 if (isset($context[$key])) {
9980 $prefix .= '{' . $key . "}\t";
9981 }
9982 }
9983 return preg_replace_callback('/{(.+?)}/', function ($match) use($context) {
9984 return array_key_exists($match[1], $context) ? $context[$match[1]] : $match[0];
9985 }, $prefix . $message);
9986 }
9987 private function rotate($bufferSize)
9988 {
9989 if (!$this->shouldRotate($bufferSize)) {
9990 return;
9991 }
9992 if (!($fp = fopen($this->path, 'r+'))) {
9993 return;
9994 }
9995 try {
9996 if (!flock($fp, LOCK_EX | LOCK_NB)) {
9997 return;
9998 }
9999 if (!$this->shouldRotate($bufferSize)) {
10000 return;
10001 }
10002 for ($i = $this->maxFiles - 1; $i > 0; $i--) {
10003 @rename($this->getName($i - 1), $this->getName($i));
10004 }
10005 } finally {
10006 fclose($fp);
10007 }
10008 }
10009 private function getName($index)
10010 {
10011 if ($index <= 0) {
10012 return $this->path;
10013 }
10014 return $this->path . '.' . $index;
10015 }
10016 private function shouldRotate($bufferSize)
10017 {
10018 $currentSize = @filesize($this->path);
10019 if (!$currentSize) {
10020 return false;
10021 }
10022 $newSize = $currentSize + $bufferSize;
10023 return $newSize > $this->maxSize;
10024 }
10025 }
10026 namespace Kibo\Phast\Logging\LogWriters\PHPError;
10027
10028 class Writer extends \Kibo\Phast\Logging\LogWriters\BaseLogWriter
10029 {
10030 private $messageType = 0;
10031 private $destination = null;
10032 private $extraHeaders = null;
10033 /**
10034 * @var ObjectifiedFunctions
10035 */
10036 private $funcs;
10037 /**
10038 * PHPErrorLogWriter constructor.
10039 * @param array $config
10040 * @param ObjectifiedFunctions $funcs
10041 */
10042 public function __construct(array $config, \Kibo\Phast\Common\ObjectifiedFunctions $funcs = null)
10043 {
10044 foreach (['messageType', 'destination', 'extraHeaders'] as $field) {
10045 if (isset($config[$field])) {
10046 $this->{$field} = $config[$field];
10047 }
10048 }
10049 $this->funcs = is_null($funcs) ? new \Kibo\Phast\Common\ObjectifiedFunctions() : $funcs;
10050 }
10051 protected function doWriteEntry(\Kibo\Phast\Logging\LogEntry $entry)
10052 {
10053 if ($this->levelMask & $entry->getLevel()) {
10054 $this->funcs->error_log($this->interpolate($entry->getMessage(), $entry->getContext()), $this->messageType, $this->destination, $this->extraHeaders);
10055 }
10056 }
10057 private function interpolate($message, $context)
10058 {
10059 $prefix = '';
10060 $prefixKeys = ['requestId', 'service', 'class', 'method', 'line'];
10061 foreach ($prefixKeys as $key) {
10062 if (isset($context[$key])) {
10063 $prefix .= '{' . $key . "}\t";
10064 }
10065 }
10066 return preg_replace_callback('/{(.+?)}/', function ($match) use($context) {
10067 return array_key_exists($match[1], $context) ? $context[$match[1]] : $match[0];
10068 }, $prefix . $message);
10069 }
10070 }