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

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