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

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