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

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